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.
- Learn React so you can describe UI as components.
- Learn Next.js so those components can render on the server (how ReactBD ships).
- Learn MongoDB so you can store documents you actually query.
- Learn Node + Express (and Nest) so the UI has an API.
- 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.
| Letter | Job | You touch it when… |
|---|---|---|
| M — MongoDB | Store documents | Users, tools, orders, anything the app mutates. |
| E — Express | HTTP on Node | Routes, middleware, status codes, JSON. |
| R — React | User interface | Pages, forms, lists, client interactivity. |
| N — Node.js | JavaScript runtime | The 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
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
The API matches a route
Express (
app.post('/api/notes', ...)) or Nest (@Post('notes')) reads the body, checks auth, validates shape (Zod). - 3
The database writes a document
collection.insertOne({ title, createdAt })returns an_id. Errors become 4xx/5xx, not a thrown HTML page. - 4
JSON comes back
The API responds
{ ok: true, data }. The UI re-renders. In Next.js you oftenrevalidatePath/revalidateTagafter 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.jsonscriptsare 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.envis how config enters the process. Never commit real secrets.
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).
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.bodyis empty.- Middleware runs in order: CORS, auth, then the route.
- Always send a status code that matches the outcome (
201created,400bad input,401unauthenticated,404missing). - 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.
| Method | Path | Meaning |
|---|---|---|
| GET | /api/public/tools | List. Cacheable. No secrets. |
| GET | /api/public/tools/:id | One document, or 404. |
| POST | /api/notes | Create. Body required. 201 + the new doc. |
| PATCH | /api/notes/:id | Partial update. 404 if missing. |
| DELETE | /api/notes/:id | Remove. 401/403 if not allowed. |
{
"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.
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
localStoragefor a first-party app — cookies withhttpOnlyexist for this.
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.
| Kind | Example | Rule |
|---|---|---|
| Server secret | MONGODB_URI, AUTH_SECRET | Never prefix with NEXT_PUBLIC_. |
| Public URL | NEXT_PUBLIC_BASE_URL | Safe in the browser. Still not a secret. |
| API origin | API_URL | Server 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.
- Create a
notescollection. IndexcreatedAt. - GET list + POST create on the API. Return
{ success, data }. - Render the list on a Next.js Server Component.
- A small client form posts, then
revalidatePath('/notes'). - Add delete behind a session. Then add tags or featured flags.
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.
@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_SECRETif 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
- You can explain what each MERN letter does in one sentence.
- You can draw the path of a POST from a form to MongoDB.
- You can write an Express (or Nest) JSON route with 400/201.
- You can render a list in React from server-fetched data.
- You know why use-client is a leaf, not a page.
- You can put a unique index on
email/slug. - You never commit secrets, and you never
NEXT_PUBLIC_a URI password.