DEV Community

Cover image for Web Transition: Part 4 of 4 — The Return to Simplicity
Raheel Shan
Raheel Shan

Posted on • Originally published at raheelshan.com

Web Transition: Part 4 of 4 — The Return to Simplicity

Introduction

Let’s recap the web’s evolution so far:

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");
Enter fullscreen mode Exit fullscreen mode

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'));
}
Enter fullscreen mode Exit fullscreen mode

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 like enhanceForm(), 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.

Support my work

Top comments (0)