The full path

MERN Stack

MongoDB, Express, React, and Node as one system — then the ReactBD adaptation: Nest on Node, Next.js App Router, and Sanity for editorial content.

Full-stackNodeExpressRESTAuthDeploy

Start here

What you will be able to build, and how to read this guide.

MERN is a JavaScript stack. One language on the browser, the server, and (with JSON/BSON) the database. That is the whole pitch: you can follow a user click from a React button, through an HTTP API, into a MongoDB document, and back — without switching languages.

This page is the map. The other study pages are the chapters. Read this once, then drill into React, Next.js, MongoDB, and Sanity. Come back here when you are ready to stitch an API and ship.

  1. Learn React so you can describe UI as components.
  2. Learn Next.js so those components can render on the server (how ReactBD ships).
  3. Learn MongoDB so you can store documents you actually query.
  4. Learn Node + Express (and Nest) so the UI has an API.
  5. Add Sanity when editors — not developers — own the words and images.

What MERN is

MERN stands for MongoDB, Express, React, and Node.js. It is not a framework you install. It is a convention: JavaScript everywhere, REST (or similar) between the UI and the API, documents in the database.

The four letters
LetterJobYou touch it when…
M — MongoDBStore documentsUsers, tools, orders, anything the app mutates.
E — ExpressHTTP on NodeRoutes, middleware, status codes, JSON.
R — ReactUser interfacePages, forms, lists, client interactivity.
N — Node.jsJavaScript runtimeThe process that runs Express/Nest and scripts.

MEAN (Angular) and MEVN (Vue) are the same idea with a different UI library. The M, E, and N do not change.

How a request travels

Follow one click from the browser to MongoDB and back.

  1. 1

    The UI fires a request

    A Server Component fetches on the server, or a client button calls fetch('/api/notes'). The body is JSON.

  2. 2

    The API matches a route

    Express (app.post('/api/notes', ...)) or Nest (@Post('notes')) reads the body, checks auth, validates shape (Zod).

  3. 3

    The database writes a document

    collection.insertOne({ title, createdAt }) returns an _id. Errors become 4xx/5xx, not a thrown HTML page.

  4. 4

    JSON comes back

    The API responds { ok: true, data }. The UI re-renders. In Next.js you often revalidatePath / revalidateTag after a mutation.

Node.js — the runtime

Node.js runs JavaScript outside the browser. Your API is a long-lived process that listens on a port (4000 locally in this monorepo). The same runtime runs build scripts, seed jobs, and the Next.js server.

  • package.json scripts are Node entry points (dev, build, start).
  • Use LTS Node (this repo targets current LTS). Do not mix major versions across apps.
  • ES modules (import) vs CommonJS (require) — pick one per package. Nest/Next both use ESM-friendly TypeScript.
  • process.env is how config enters the process. Never commit real secrets.
hello-server.ts
import { createServer } from "node:http";

const port = Number(process.env.PORT) || 4000;

createServer((req, res) => {
  res.writeHead(200, { "content-type": "application/json" });
  res.end(JSON.stringify({ ok: true, path: req.url }));
}).listen(port, () => {
  console.log(`API on :${port}`);
});

Express — classic HTTP

Express is a thin layer on Node: routing, middleware, request/response. Almost every Node tutorial starts here. Nest sits on the same ideas (it can even use the Express adapter).

server.ts
import express from "express";
import { MongoClient } from "mongodb";

const app = express();
app.use(express.json());

const client = new MongoClient(process.env.MONGODB_URI!);
const notes = client.db().collection("notes");

app.get("/api/notes", async (_req, res) => {
  const data = await notes.find().sort({ createdAt: -1 }).toArray();
  res.json({ success: true, data });
});

app.post("/api/notes", async (req, res) => {
  const title = String(req.body?.title ?? "").trim();
  if (!title) {
    res.status(400).json({ success: false, error: "title required" });
    return;
  }
  const doc = { title, createdAt: new Date() };
  const { insertedId } = await notes.insertOne(doc);
  res.status(201).json({ success: true, data: { _id: insertedId, ...doc } });
});

app.listen(4000);
  • express.json() parses JSON bodies. Without it, req.body is empty.
  • Middleware runs in order: CORS, auth, then the route.
  • Always send a status code that matches the outcome (201 created, 400 bad input, 401 unauthenticated, 404 missing).
  • Keep handlers async and wrap errors — an unhandled rejection takes down the process.

REST APIs that stay honest

REST here means: resources over HTTP, JSON in and out, status codes that mean something. You do not need a 20-resource textbook. You need a catalog that lists, a detail that fetches one, and writes that are authenticated.

MethodPathMeaning
GET/api/public/toolsList. Cacheable. No secrets.
GET/api/public/tools/:idOne document, or 404.
POST/api/notesCreate. Body required. 201 + the new doc.
PATCH/api/notes/:idPartial update. 404 if missing.
DELETE/api/notes/:idRemove. 401/403 if not allowed.
envelope.json
{
  "success": true,
  "data": [],
  "count": 0
}

React in a MERN app

In classic MERN, React is a SPA (Vite or CRA) that talks to Express. In ReactBD, React runs inside Next.js: most pages are Server Components; use-client is a leaf for buttons, forms, and menus.

MongoDB in a MERN app

MongoDB stores documents in collections. Shape the document for how you read it. Put a unique index on emails and slugs. Use aggregation when you need counts and groups — not a second database.

notes.insert.js
db.notes.insertOne({
  title: "Ship the public catalog",
  tags: ["api", "mongo"],
  featured: false,
  createdAt: ISODate()
})

Auth — sessions, not hope

Auth is a server concern. The browser holds a cookie. The API trusts that cookie only after verifying the signature. OAuth (Google, GitHub) belongs on the parent app so subdomains cannot steal the session.

  • ReactBD uses Auth.js (NextAuth) with a JWT session cookie.
  • The Nest API verifies the same secret the Next app signs with.
  • Public GETs skip auth. Writes call requireUser / requireAdmin.
  • Never put access tokens in localStorage for a first-party app — cookies with httpOnly exist for this.
require-user.ts
export async function requireUser() {
  const session = await auth();
  if (!session?.user) {
    throw new Error("Unauthorized");
  }
  return session.user;
}

Environment and config

Local, preview, and production are the same code with different env files. This monorepo loads root .env then overlays .env.development or .env.production.

KindExampleRule
Server secretMONGODB_URI, AUTH_SECRETNever prefix with NEXT_PUBLIC_.
Public URLNEXT_PUBLIC_BASE_URLSafe in the browser. Still not a secret.
API originAPI_URLServer fetches use this. CORS must allow the web origin.

A first full-stack slice

One resource, end to end, before you add a UI kit.

Build notes: list, create, delete. That slice teaches routing, validation, Mongo, and a form. Do not start with auth providers and a design system.

  1. Create a notes collection. Index createdAt.
  2. GET list + POST create on the API. Return { success, data }.
  3. Render the list on a Next.js Server Component.
  4. A small client form posts, then revalidatePath('/notes').
  5. Add delete behind a session. Then add tags or featured flags.
app/notes/page.tsx
import { api } from "@/lib/api";

export default async function NotesPage() {
  const res = await fetch(`${api}/notes`, {
    next: { revalidate: 60, tags: ["notes"] },
  });
  const { data } = await res.json();

  return (
    <ul>
      {data.map((note: { _id: string; title: string }) => (
        <li key={note._id}>{note.title}</li>
      ))}
    </ul>
  );
}

NestJS instead of Express

Express is one file until it is not. NestJS keeps HTTP, but adds modules, controllers, providers, and a predictable folder layout. ReactBD’s API (apps/api) is Nest on Node — still MERN, still JSON over HTTP.

tools.controller.ts
@Controller("public/tools")
export class ToolsController {
  constructor(private readonly tools: ToolsService) {}

  @Get()
  async list() {
    const data = await this.tools.findPublic();
    return { success: true, data, count: data.length };
  }

  @Get(":id")
  async one(@Param("id") id: string) {
    const tool = await this.tools.findById(id);
    if (!tool) throw new NotFoundException();
    return { success: true, data: tool };
  }
}

Next.js instead of a SPA

A Vite SPA sends an empty shell and fetches everything in the browser. Next.js App Router renders HTML on the server, streams it, then hydrates only the client leaves. Same React. Better first paint, better SEO.

This study site is static data — it should be generated on the server. Product pages that read Mongo or Sanity still fetch in a Server Component, not in useEffect.

Sanity for editorial content

MongoDB is for application data (users, tools, shops). Sanity is for editorial documents (posts, courses, landing copy) that non-developers edit in Studio. Mixing them is the usual production MERN move — not a betrayal of the stack.

Deploy

  • Web and admin: Next.js on Vercel (or any Node host).
  • API: Nest as a long-lived Node process (container, VM, or platform).
  • MongoDB: Atlas (or a self-hosted replica set). Allowlist IPs or use VPC peering.
  • Sanity: hosted Studio + Content Lake. The Next app queries the CDN.
  • Env: production overlay only. Rotate AUTH_SECRET if it ever leaked.

A deploy is not done until health checks pass: web / 200, API public catalog 200, Mongo ping, Sanity query. ReactBD keeps those checks in lib/db-health.

Beginner checklist

  1. You can explain what each MERN letter does in one sentence.
  2. You can draw the path of a POST from a form to MongoDB.
  3. You can write an Express (or Nest) JSON route with 400/201.
  4. You can render a list in React from server-fetched data.
  5. You know why use-client is a leaf, not a page.
  6. You can put a unique index on email / slug.
  7. You never commit secrets, and you never NEXT_PUBLIC_ a URI password.

Related

Part of the ReactBD MERN documentation. Static content, rendered on the server.