DEV Community

Cover image for How to Fix an Over-Engineered Frontend (When Plain HTML Was Enough)
Alan West
Alan West

Posted on

How to Fix an Over-Engineered Frontend (When Plain HTML Was Enough)

Every few months, I watch a junior dev spin up a new React app with Next.js, Tailwind, a state management library, and three different build tools — for what turns out to be a mostly static page with a contact form.

I've been building for the web since the jQuery days. And look, I genuinely like React. But I've also shipped projects where the framework was the problem, not the solution. The real issue isn't nostalgia — it's that we've lost the ability to diagnose when our tooling is working against us.

Let me walk you through how to recognize an over-engineered frontend and what to do about it.

The Symptoms

You know your frontend stack is fighting you when:

  • Your build step takes longer than your deploy
  • You have more config files than actual page templates
  • node_modules is larger than your entire backend
  • You're debugging hydration mismatches on a page that barely has interactivity
  • New team members need a full day just to understand the dev setup

I hit this exact wall last year on a client dashboard project. We'd started with Next.js because "we might need SSR later." Six months in, we had 47 dependencies, a 90-second build, and exactly zero pages that actually needed client-side rendering. The whole thing could have been server-rendered HTML with a couple of <script> tags.

The Root Cause: Defaulting to Complexity

The real problem isn't any specific framework. It's that our industry has normalized starting every project at maximum complexity. We reach for a SPA framework before we've even asked the fundamental question: does this page need to be an application, or is it a document?

Most content on the web is documents. Blog posts, marketing pages, dashboards that display data, admin panels with forms. These don't need a virtual DOM. They don't need client-side routing. They need HTML that the server sends and the browser renders.

The old school devs weren't wrong — they were just solving the right problem with the right tool.

Step 1: Audit Your Interactivity

Before ripping anything out, figure out what actually needs JavaScript. I use a simple test: open your app, disable JavaScript in the browser, and see what breaks.

<!-- This doesn't need React. It's a form. -->
<form action="/api/contact" method="POST">
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>

  <!-- HTML validation is shockingly capable now -->
  <button type="submit">Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

You'd be surprised how much of your UI works without JavaScript at all. Native HTML form validation, <details> for accordions, CSS for animations — the platform has caught up to a lot of what we used to need jQuery for.

Step 2: Replace Framework Features with Platform Features

Modern HTML and CSS handle things that used to require a library. Here's a modal dialog that would have been a React component with state management:

<!-- Native dialog element — no JS library needed -->
<dialog id="confirm-dialog">
  <h2>Are you sure?</h2>
  <p>This action cannot be undone.</p>
  <form method="dialog">
    <!-- method="dialog" closes the dialog and returns the value -->
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

<button onclick="document.getElementById('confirm-dialog').showModal()">
  Delete Item
</button>

<style>
  /* The ::backdrop pseudo-element handles the overlay */
  dialog::backdrop {
    background: rgba(0, 0, 0, 0.5);
  }

  dialog {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 2rem;
    max-width: 400px;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

No useState. No useEffect. No portal. No accessibility library — the <dialog> element handles focus trapping and escape-key dismissal natively.

Step 3: Use Server-Side Rendering Where It Belongs

If your backend already has all the data, why send JSON to the client just to template it into HTML there? Cut out the middleman.

Most backend frameworks have excellent templating. Pick your language's standard option:

  • Python: Jinja2 with Flask or Django templates
  • Go: html/template in the standard library
  • Ruby: ERB with Rails
  • PHP: Blade with Laravel (or just... PHP, which is literally a template language)
  • Node: EJS, Pug, or Handlebars with Express
# Flask example — the entire "frontend" is server-rendered
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/dashboard")
def dashboard():
    stats = get_dashboard_stats()  # your existing backend logic
    # Template receives data directly — no API layer needed
    return render_template("dashboard.html", stats=stats)
Enter fullscreen mode Exit fullscreen mode

You just eliminated your API layer, your client-side state management, your loading spinners, and your hydration bugs. The browser gets HTML. It renders HTML. Done.

Step 4: Add Interactivity Surgically

For the parts that genuinely need client-side interactivity, you don't have to go full SPA. Libraries like htmx or Alpine.js let you add behavior to server-rendered HTML without a build step.

But honestly? Vanilla JavaScript is fine for most things.

// A lightweight search filter — no framework required
const searchInput = document.querySelector("#search");
const items = document.querySelectorAll(".item");

searchInput.addEventListener("input", (e) => {
  const query = e.target.value.toLowerCase();

  items.forEach((item) => {
    // Toggle visibility based on text content match
    const matches = item.textContent.toLowerCase().includes(query);
    item.style.display = matches ? "" : "none";
  });
});
Enter fullscreen mode Exit fullscreen mode

Twelve lines. No dependencies. No build step. Works in every browser.

When You Actually Need a Framework

I'm not saying burn all your React code. Frameworks earn their keep when you have:

  • Highly interactive UIs — think Figma, Google Docs, or complex data visualization
  • Real-time collaborative features where multiple users modify shared state
  • Complex client-side state — multi-step wizards, drag-and-drop interfaces, offline-first apps
  • Large teams where component-based architecture helps with code organization and ownership

If your app has a rich text editor, a canvas-based tool, or real-time multiplayer features, absolutely use a framework. That's what they were designed for.

The mistake is using them for everything else too.

Prevention: The 5-Minute Rule

Before starting your next project, spend five minutes with a blank HTML file. Seriously.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Project</title>
    <style>
      /* Start here. See how far you get. */
      body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 1rem; }
    </style>
</head>
<body>
    <h1>Hello</h1>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Open it in a browser. No build step. No waiting. Instant feedback. Now ask yourself: at what point does this project actually need a framework? You might be surprised how far raw HTML, CSS, and a server-rendered template get you.

The old school approach wasn't primitive. It was simple. And simple is a feature that most modern stacks have accidentally optimized away.

Top comments (0)