Skip to content
Back to blog
Architecture

Designing a Scalable MERN Architecture

How I structure MERN applications so features stay cheap to add and bugs stay easy to isolate — from data modelling to typed API clients.

March 12, 2026 2 min read

Most MERN tutorials stop at "it works." Production is a different game — the question is whether the next feature is cheap to add. Here's the structure I reach for.

Start with the data model

Before a single component, I model the data. Clear collections and well-defined relationships make everything downstream simpler.

models/booking.ts
import { Schema, model } from "mongoose";
 
const BookingSchema = new Schema(
  {
    user: { type: Schema.Types.ObjectId, ref: "User", required: true },
    trip: { type: Schema.Types.ObjectId, ref: "Trip", required: true },
    status: {
      type: String,
      enum: ["pending", "confirmed", "cancelled"],
      default: "pending",
    },
  },
  { timestamps: true },
);
 
export const Booking = model("Booking", BookingSchema);

A typed API client beats scattered fetch calls

A single typed client gives every component the same contract and the compiler catches mismatches before runtime.

lib/api.ts
export async function getBookings(): Promise<Booking[]> {
  const res = await fetch("/api/bookings");
  if (!res.ok) throw new Error("Failed to load bookings");
  return res.json();
}

Boundaries, not layers

I separate by feature, not by technical layer. Each feature owns its routes, controllers, and types — so deleting a feature is a folder delete, not an archaeology expedition.

The cheapest reviewer on the team is the TypeScript compiler. Type end to end and let it work for you.

That's the core of it: model first, type everything, and keep boundaries clean.

#MERN#Architecture#Node.js#MongoDB