DEV Community

Cover image for Why We Built a Frontend That Outlives the Project
Jonas Landgård
Jonas Landgård

Posted on • Originally published at Medium

Why We Built a Frontend That Outlives the Project

Every new client e-commerce project felt like déjà vu. Different brands, different designs — yet the same foundational code had to be rebuilt from scratch: cart logic, product lists, authentication, checkout state. Each project started with weeks of boilerplate before any real design work began. As our client base grew, so did the maintenance burden. Fixing a bug in one storefront meant repeating the same patch across several others. Over time, our frontend code became the bottleneck, not the enabler, of our delivery speed.

The agency paradox

As an agency working with many e‑commerce clients, we needed both speed and freedom. Each customer deserved a unique brand experience, but under the hood, 70–80 % of the logic was identical. We wanted to reuse that shared logic without locking designers or developers into a specific look, stack, or framework. Traditional theme systems and component libraries couldn’t give us that balance. They saved time early but made innovation harder later.

Our first attempt (and why it didn’t work)

Our first attempt was a shared component library. It seemed reasonable: one source of truth for all UI parts. But as the library grew, it became harder to maintain. The QuantityButtons component from our old ecom-components repo perfectly captured the pain. It used more than 30 CSS variables, had a fixed DOM structure, and styling leaked between nested components. Overriding colors or layouts required deep selector hacks and !important rules. We realised reuse at the component level wasn’t flexible enough. We needed reuse at the logic level.

In short: Logic and presentation were tangled, which slowed every new project and created unnecessary technical debt.

Before: monolithic component

// Before: monolithic UI component
// – tightly coupled markup, styling and behavior
// – theming required CSS overrides and !important hacks
// – every project had to rebuild or restyle this component
export function QuantityButtons({ className, theme, ...props }) {
  return (
    <div className={`qty ${theme} ${className}`}>
      <button className="decrement">–</button>
      <input className="input" type="number" {...props} />
      <button className="increment">+</button>
    </div>
  )
}
// Over 30 CSS variables and nested selectors to adapt styling per project
Enter fullscreen mode Exit fullscreen mode

This approach caused endless style collisions and made it nearly impossible to isolate design systems between clients.

Our headless frontend pattern

The breakthrough was treating the frontend the same way headless commerce treats the backend — by separating logic from presentation. Each domain became its own headless module: hooks like useCart, useProductList, or useQuantityButtons handle state, validation, and actions, but make no assumptions about how the UI looks. Developers can compose their own markup using these hooks or use our prebuilt composition patterns. This means our logic layer behaves like an API: stable, documented, versioned, and UI‑agnostic.

What this means: The logic is universal; the experience is unique. Each project can render the same data and state logic in completely different ways.

After: headless logic + composition

export const QuantityButtons = {
  Root: ({ value, onValueChange, children, ...props }) => {
    const hookValues = useQuantityButtons({ value, onValueChange })
    return (
      <QuantityButtonsContext.Provider value={hookValues}>
        <div {...props}>{children}</div>
      </QuantityButtonsContext.Provider>
    )
  },
  Decrement: (props) => {
    const { getDecrementProps } = useQuantityButtonsContext()
    return <button {...getDecrementProps(props)} />
  },
  Increment: (props) => {
    const { getIncrementProps } = useQuantityButtonsContext()
    return <button {...getIncrementProps(props)} />
  },
  Input: (props) => {
    const { getInputProps } = useQuantityButtonsContext()
    return <input {...getInputProps(props)} />
  },
}
Enter fullscreen mode Exit fullscreen mode

Logic and UI are now fully decoupled. The hook layer provides the logic, while the composition pattern allows each client project to define its own markup and styling.

Usage in a client project

<QuantityButtons.Root
  value={quantity}
  onValueChange={handleQuantityChange}
  className="product-quantity"
>
  <QuantityButtons.Decrement className="decrement-btn" />
  <QuantityButtons.Input unit="st" className="quantity-field" />
  <QuantityButtons.Increment className="increment-btn" />
</QuantityButtons.Root>
Enter fullscreen mode Exit fullscreen mode

This is how client teams can implement their own brand‑specific UI without touching the shared logic.

Same logic, different UI. Three ways of rendering the quantityButton component.

How we structured it

We structure our storefront as a set of modular, versioned npm packages that can be mixed and matched depending on project needs.

The monorepo layout

haus-storefront-components/
  ├─ core/                 # shared types, config, GraphQL plumbing
  ├─ cart/                 # cart hooks, reducers, mutations
  ├─ search/               # product search and filters
  ├─ auth/                 # login, registration, account state
  └─ ...                   # other business domains
Enter fullscreen mode Exit fullscreen mode

Each package owns its own version and changelog. They are published independently, which lets us upgrade only the modules that matter to a given project.

Dependency flow

  1. Core provides base utilities and shared contracts.
  2. Each domain package imports core and exposes typed hooks (useCart, useSearch, etc.).
  3. Client projects or integrations import these packages as peer dependencies.
  4. The monorepo is managed with Changesets to ensure semantic versioning and safe publishing.

Client repositories

Each client repository contains only what’s needed for its own storefront:

  • a component manifest describing which shared packages and hooks are in use,
  • a UI layer implementing brand‑specific markup and styling built on top of those hooks,
  • any brand assets, translations, and deployment configuration.
// package.json (client repo)
{
  "dependencies": {
    "@haus/cart": "^2.4.1",
    "@haus/catalog": "^1.9.0"
  }
}
// → patch @haus/cart to 2.4.2 without touching @haus/catalog
Enter fullscreen mode Exit fullscreen mode

When we push an update to, say, @haus/cart@2.4.1, the client repo automatically receives it via dependency update. Fixes and new features propagate predictably without rewriting UI code.

Integration layer

We maintain lightweight integration bridges that adapt the same headless packages to different environments —anything fromcustom React apps to WordPress/Elementor widgets. The core logic is identical; only the rendering and data‑wiring differ. This makes it possible to reuse the same commerce logic across multiple platforms, from marketing sites to mobile apps.

Results

For an agency, speed isn’t just about code — it’s about margin, delivery risk, and client trust.

The results were immediate:

  • Onboarding time for new projects dropped from several weeks to a day or so.
  • Consistency improved; a fix or new feature in one module benefits every client.
  • Design freedom increased, since the UI layer is fully independent of the logic layer.
  • Maintenance became predictable and version‑controlled. We can patch @core/cart without touching product lists or search.

When we tested the architecture, we built a complete demo storefront from scratch: product pages, category listings, cart, and login.

It took one developer two days, and almost all that time went into styling. The logic was already in place. Once styled, that setup becomes a boilerplate. Spinning up a new project now takes minutes.

Why this matters for agencies

For agencies, the difference is profound. You no longer rebuild the same e‑commerce logic under new branding each time. Modular, versioned packages turn the frontend into a maintainable product that evolves independently from any single client. It’s a mindset shift: from project delivery to platform engineering. Agencies can deliver faster, support multiple clients more efficiently, and still craft unique brand experiences.

Discussions about composable frontend architecture are becoming more common, from practical guides on freeCodeCamp, to platform-oriented insights from Mia-Platform, and developer perspectives on Directus and DEV.

These articles explore modularisation, micro-frontends, and shared dependencies. Most focus on single-product contexts.

Our challenge was different: as an agency, we needed a composable frontend architecture that works across many client projects.

Instead of treating the frontend as a single modular app, we treat it as a set of versioned packages that can power ten different storefronts, each with its own design system and deployment pipeline.

That shift, from one modular app to a shared logic layer powering many independent storefronts, is what makes our approach unique.

Looking ahead

The idea of a frontend that outlives any single project started as a technical refactor, but it ended up changing how we think about building digital products altogether. Instead of disposable codebases, we now have a living system that powers multiple experiences across platforms.

The next logical step is to let AI build on top of this foundation — not to invent logic, but to compose new experiences. With every module versioned and proven in production, an AI could safely generate complete storefronts in near real time by assembling layout, flow, and design from battle-tested logic.

The outcome: faster launches, tighter feedback loops, and more space for human creativity.

Because this approach doesn’t just change how we code — it changes how we build, scope, deliver, and evolve digital products. And it lets our developers focus on what truly matters: creating new value, not rebuilding the old.

If you want to see more implementation details, we’re planning a follow‑up deep dive on dependency management, type safety, and testing strategies for modular frontends. Feedback and questions are welcome. Drop a comment or connect with me on LinkedIn to continue the discussion.

Top comments (0)