Introduction
Let’s recap the web’s evolution so far:
- Part 1: The backend ruled — it rendered pages, handled routing, and managed all logic.
- Part 2: AJAX and jQuery entered, letting us update parts of the page without reloads.
- Part 3: SPAs took over — shifting routing, validation, and rendering to the frontend. The JavaScript ecosystem exploded with complexity.
Now in Part 4, we’re witnessing a new shift: A return to server-first thinking, progressive enhancement, and leaner web architecture — not by going backward, but by merging the best of both worlds.
We’re building apps that are fast, interactive, and SEO-friendly — without overloading the browser.
The Problems SPAs Created
SPAs solved real pain points: smooth routing, rich interactivity, app-like behavior. But they came at a cost:
- Every
<form>
had to be hijacked by JavaScript - Data fetching got entangled in
useEffect
,useQuery
, and custom hooks - Routing became entirely JS-driven
- Validation was duplicated in frontend and backend
- Pages stayed blank until hydration completed
- Massive JS bundles hurt performance and load times
- SEO suffered without serious workaround
- Build processes became fragile and overly complex
In trying to build better user experiences, we made development harder and websites slower.
Server-First, Client-Smart
Instead of dumping everything on the client, we’re swinging back — giving the server responsibility for what it's always done best:
- Routing
- Validation
- Data handling
- Rendering UI
But with a twist: We still use JavaScript — selectively, surgically, and wisely.
This approach is often called progressive enhancement or HTML-over-the-wire. It offers:
- Faster initial page loads
- Smaller JS bundles
- Improved SEO
- Simpler architecture
The New Balance
This new transition creates a balanced contract:
The server:
- Accepts and validates form data
- Handles routing
- Returns HTML or JSON
- Manages state and rendering logic
The frontend:
- Handles interactivity enhancements
- Submits forms via
fetch()
- Updates DOM elements intelligently
- Doesn't reimplement server logic
We're not going back to full-page reloads. We’re just letting the browser and server do what they were designed for — with JavaScript filling in the gaps.
A Real-World Example Using fetch()
Let’s say you have a native HTML form. Instead of relying on a full-page reload or wrapping it in React or Vue logic, you can enhance it like this:
function enhanceForm({
formId,
targetId,
method = null,
swap = "innerHTML",
beforeSend = null,
onSuccess = null,
onError = null
}) {
const form = document.getElementById(formId);
const target = document.getElementById(targetId);
if (!form || !target) return;
form.addEventListener("submit", async function (e) {
e.preventDefault();
if (typeof beforeSend === "function") beforeSend(form);
const formData = new FormData(form);
const reqMethod = method || form.method || "POST";
try {
const response = await fetch(form.action, {
method: reqMethod.toUpperCase(),
headers: { 'X-Requested-With': 'fetch' },
body: formData
});
const contentType = response.headers.get("content-type") || "";
const result = contentType.includes("application/json")
? await response.json()
: await response.text();
if (response.ok) {
if (typeof onSuccess === "function") onSuccess(result);
else applySwap(target, result, swap);
} else {
if (typeof onError === "function") onError(result, response.status);
else applySwap(target, `<strong>Error:</strong> ${response.status}`, swap);
}
} catch (err) {
if (typeof onError === "function") onError(err.message, 0);
else applySwap(target, `<strong>Network Error:</strong> ${err.message}`, swap);
}
});
}
function applySwap(target, content, swap) {
switch (swap) {
case "outerHTML": target.outerHTML = content; break;
case "append": target.insertAdjacentHTML("beforeend", content); break;
case "prepend": target.insertAdjacentHTML("afterbegin", content); break;
case "innerHTML":
default: target.innerHTML = content;
}
}
enhanceForm("myForm", "result");
Now the server can respond with HTML (or JSON), and the frontend simply swaps it into the DOM.
What Does the Server Do?
In this model, the server:
- Handles form submissions
- Performs validation
- Renders HTML fragments or JSON responses
- Controls routing and redirection
- Persist Data
Here’s how that might look in Laravel:
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required',
'email' => 'required|email'
]);
$user = User::create($validated);
return view('partials.user-card', compact('user'));
}
The frontend then drops this HTML into a specific section of the page.
What Does the Client Do?
While we’ve handed back most responsibilities to the server, the client still plays an essential role — just not everything like in the SPA era.
Here's what the browser is responsible for now:
- UI Enhancements: The client enhances user experience where needed — like toggling modals, auto-focusing inputs, or handling optimistic UI updates. But it’s no longer overburdened with rendering entire pages.
- Submitting Forms via JavaScript: Using
fetch()
or tools likeenhanceForm()
, the client intercepts form submissions to: Prevent full-page reloads, Show loading indicators, swap in HTML snippets or process JSON
- DOM Updates: Instead of React’s virtual DOM diffing entire component trees, the client just updates specific parts of the page — like replacing a list, form section, or message area. It’s precise and fast.
- Handling Client-Only Interactions: Some things still belong to the client — think dropdowns, drag-and-drop, keyboard shortcuts, and local state toggles. These don’t need the server.
- Making Additional Data Requests: Need more data after the page loads? The client can still use
fetch()
to talk to APIs — just without wrapping it in giant state libraries or effect chains.
What Stacks Are Doing This?
This isn't just a theory — modern stacks are already embracing it.
JavaScript World:
- Remix – Form-first, progressive enhancement built-in
- SolidStart – Progressive-first rendering
- SvelteKit – Server rendering with smart enhancement
PHP Ecosystem:
- Laravel Livewire – HTML-over-the-wire with backend logic
- Inertia.js – Uses Vue/React, but driven by backend routes
- HTMX – Pure HTML-over-the-wire enhancement
- Alpine.js – Lightweight interactivity on top of server-rendered HTML
ASP.NET:
- Blazor Server – Server-driven components with real-time DOM updates
- Razor Pages + fetch – Very close to this form-enhancement philosophy
Where We’ve Landed
After years of experimentation and overcomplication, we’re returning to a simpler truth:
Backend Responsibilities
- Data persistence
- Routing
- Validation
- Business logic
- Rendering
Frontend Responsibilities
- DOM updates
- Form enhancements
- Selective interactivity
Final Words
We didn’t go full circle — we evolved. From monolithic servers, to client-heavy SPAs, and now toward smarter, hybrid applications that blend the strengths of both.
The result? Simpler, faster, more maintainable apps — and a web that feels native again.
If you found this post helpful, consider supporting my work — it means a lot.
