Architecture

How I Structure a Nuxt + API + CMS Stack

Practical architecture for real client projects

When I start a new client project, I'm not thinking about which technologies are exciting. I'm thinking about who will maintain this in two years, what will break first, and how much it will cost to change direction.

That framing leads to a fairly consistent architecture. Not because it's the only way, but because it handles the constraints I encounter most often.

The Layers

My typical stack has three clear layers:

Frontend (Nuxt 3). This is what users see. It handles routing, rendering, and presentation. It knows nothing about business logic beyond what's needed to display content.

API layer. This sits between the frontend and everything else. It handles authentication, data transformation, and integration with external services. It's deliberately boring.

Content layer (CMS + Database). This is where content lives. Sometimes it's a headless CMS like Sanity or Storyblok. Sometimes it's a custom setup with Supabase. The choice depends on who's editing content and how structured it needs to be.

What Belongs Where

The hardest decisions aren't about technology. They're about boundaries.

Frontend responsibilities:

  • Routing and navigation
  • Component rendering and state management
  • Form validation (client-side)
  • UI interactions and animations
  • SEO meta and structured data

API responsibilities:

  • Authentication and session management
  • Data validation (server-side)
  • Business logic that shouldn't live in the frontend
  • Integration with payment providers, email services, etc.
  • Rate limiting and security controls

CMS responsibilities:

  • Content storage and versioning
  • Editorial workflows
  • Media management
  • Content modelling and relationships

The goal is clear ownership. When something breaks, there's no ambiguity about where to look.

Why APIs Should Be Boring

I've inherited projects where the API layer was "clever." Custom query languages. Automatic schema generation. Dynamic resolvers that figured out what you wanted.

They were impressive when built. They were nightmares to debug.

My APIs are boring. REST endpoints that do one thing. Predictable responses. Explicit error handling. Documentation that a junior developer can follow.

This isn't about capability — it's about maintenance. The person debugging this at 2am shouldn't need to understand metaprogramming to fix a broken endpoint.

Avoiding Tight Coupling

Tight coupling is the silent killer of maintainability. Some patterns I avoid:

CMS-specific component names. The frontend shouldn't know it's talking to Sanity vs Contentful. It receives content in a normalised format and renders it. If we change CMSs, the frontend shouldn't care.

Direct database access from frontend. Even with Supabase making this easy, I route through an API layer. It adds a hop but centralises security and makes the data layer replaceable.

Shared types that create deployment dependencies. If changing a type in the CMS requires redeploying the frontend, you've coupled your release cycles. That coupling compounds over time.

Designing for Change

The one constant in client projects is that requirements change. The CMS that was perfect in month one becomes limiting in month twelve. The payment provider gets acquired. The API that worked fine starts returning different data.

My architecture assumes this. Not by over-engineering abstractions, but by keeping layers thin and boundaries clear. When change comes, it's contained.

A practical example: I use adapter patterns for external services. The frontend calls a generic "submitPayment" method. The API implements that method for Stripe today. Tomorrow, if we switch to Adyen, only the adapter changes. The frontend never knows.

What I Don't Do

Some things I've stopped including in client architectures:

  • GraphQL for simple content sites (REST is simpler and sufficient)
  • Microservices for teams of five (the overhead isn't justified)
  • Custom authentication when Auth0 or Supabase Auth works fine
  • Real-time features that could be polling (WebSockets add complexity)
  • Kubernetes for projects that could run on Vercel or Netlify

Each of these is the right choice sometimes. But defaulting to simplicity and adding complexity only when forced has served me better than the reverse.

The Result

Projects built this way aren't exciting to describe. They don't have novel architecture diagrams or bleeding-edge dependencies.

But they ship on time. They're maintainable by teams who didn't build them. And when requirements change — as they always do — the changes are contained and predictable.

That's the goal. Not impressive architecture. Sustainable architecture.

Need Architecture Help?

I help teams design systems that last beyond the initial build.

HD
Headless Digital

Senior, hands-on, and accountable. No inflated teams. No unnecessary layers.

Connect

© 2025 Headless Digital. All rights reserved.

Built withNuxt & Tailwind