A real app, end to end.

Most MERN tutorials show you a todo app on localhost and call it done. This path takes the same app all the way to a live URL — real auth, a real database, real deployment gotchas, and the production checklist that separates a demo from something you'd trust with real users.

Duration
8 weeks
Format
One app, built in layers
Prerequisite
JavaScript track
You'll build
A deployed full-stack app

One app, built one real layer at a time

Every stage adds one real layer to the same app — Node, then Express, then MongoDB, then auth, then React, then the connection between them, then deployment, then production hardening.

A live playground, right here

No setup, no account. This is Stage 1's Express middleware chain, simplified to run without a real server — edit it, break it, fix it.

playground.js
Console
Run the code to see output here.

This is what's waiting at the end

Certificate of Completion
Your Name
has completed A Real App, End to End — 9 stages, 9 passed quizzes
9 / 9 stagesDeployed appVerified ID
🔒

Complete all 9 stages and their quizzes to unlock this.

Nine stages, in this order

Each stage leans on the one before it — auth doesn't make sense until you have a real API, and deployment doesn't make sense until you have something worth deploying.

STAGE 00

Node.js fundamentals & the event loop

Week 1

Most tutorials treat Node as “JavaScript but for servers” without explaining why it can handle thousands of concurrent connections on a single thread — that understanding is what separates someone who can debug a hanging server from someone who can't.

What you'll learn
  • The event loop, applied server-side this time — non-blocking I/O specifically
  • CommonJS vs. ES modules in Node, and npm/package.json fundamentals
  • What Express is actually abstracting away, by seeing what's underneath it first
Code
// Blocking — the whole process waits here
const data = fs.readFileSync("large-file.txt");
console.log("This waits for the read to finish");

// Non-blocking — Node moves on immediately
fs.readFile("large-file.txt", (err, data) => {
  console.log("This runs when the read finishes, later");
});
console.log("This logs FIRST, before the file is read");
BuildA plain HTTP server using Node's built-in http module — no Express yet, so you see exactly what Express saves you from writing by hand.
Self-checkWhy doesn't a single-threaded Node server fall over under 1,000 concurrent requests, when a single thread obviously can't run 1,000 things at once?
STAGE 01

Express — building a real REST API

Week 1–2

Express tutorials usually show routes in isolation. Real APIs need middleware, error handling, and route organization from day one, or they become unmaintainable by the tenth route.

What you'll learn
  • Routing and middleware — what middleware actually is: a function with next()
  • Centralized error-handling middleware instead of try/catch in every route
  • Organizing routes into separate files as the API grows
Code
app.use(express.json());

// Middleware: runs before the route handler, calls next() to continue
function logRequest(req, res, next) {
  console.log(`${req.method} ${req.path}`);
  next(); // forgetting this hangs the request forever
}

app.get("/api/todos", logRequest, (req, res) => {
  res.json(todos);
});

// Error-handling middleware — 4 arguments is what makes Express treat it specially
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});
BuildA full REST API for a “todos” resource — GET/POST/PUT/DELETE, proper status codes, organized into its own router file.
Self-checkWhat does calling next() actually do, and what happens if middleware forgets to call it?
STAGE 02

MongoDB & Mongoose — modeling data that isn't a spreadsheet

Week 2–3

Coming from SQL (or nothing), the instinct is to model MongoDB like relational tables. The actual skill is knowing when to embed vs. reference data — that decision reshapes your whole schema.

What you'll learn
  • Documents and collections, and how they differ from rows and tables
  • Schema design: embedding vs. referencing, and the tradeoffs of each
  • Mongoose schemas, validation, and basic queries
Code
const todoSchema = new mongoose.Schema({
  title: { type: String, required: true },
  done: { type: Boolean, default: false },
  userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, // reference
  createdAt: { type: Date, default: Date.now },
});

const Todo = mongoose.model("Todo", todoSchema);

// Basic query
const userTodos = await Todo.find({ userId: currentUserId }).sort({ createdAt: -1 });
BuildConnect the Stage 1 todos API to a real MongoDB database via Mongoose — replace the in-memory array with real persistence.
Self-checkYou're modeling blog posts and comments. When would you embed comments inside the post document, and when would you reference them separately?
STAGE 03

Authentication — sessions, JWTs, and why passwords are hard

Week 3–4

“Just use bcrypt and JWT” is advice people follow without understanding what problem either one actually solves — which is exactly how security bugs happen.

What you'll learn
  • Password hashing — why plaintext or reversible encryption is always wrong
  • JWT structure and what “stateless” authentication actually means
  • Session vs. token tradeoffs, and protecting routes with middleware
Code
// Signup: hash before storing, never store plaintext
const hash = await bcrypt.hash(password, 10);
await User.create({ email, passwordHash: hash });

// Login: issue a JWT after verifying
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return res.status(401).json({ error: "Invalid credentials" });
const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, { expiresIn: "7d" });

// Protect a route
function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  try {
    req.userId = jwt.verify(token, process.env.JWT_SECRET).sub;
    next();
  } catch {
    res.status(401).json({ error: "Unauthorized" });
  }
}
BuildAdd real signup/login to the todos API — hashed passwords, a JWT issued on login, and routes protected by auth middleware.
Self-checkWhy can't you “un-hash” a bcrypt password to recover the original, but you CAN decode a JWT's payload without any secret?
STAGE 04

React fundamentals — components, props, state

Week 4–5

You already understand closures and the DOM from the JavaScript track, so React's mental model — components as functions, state as “what triggers a re-render” — clicks fast. This stage moves quicker than a from-zero React course.

What you'll learn
  • Components as functions, and why props only flow one direction
  • useState, and what “triggers a re-render” actually means
  • Controlled inputs — the form pattern React expects
Code
function TodoInput({ onAdd }) {
  const [text, setText] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    if (!text.trim()) return;
    onAdd(text);
    setText("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button type="submit">Add</button>
    </form>
  );
}
BuildA todo list UI — add, toggle, delete — using local state only, no API connection yet.
Self-checkWhy doesn't calling setState update the variable immediately within the same function call?
STAGE 05

React + API — data fetching, loading, errors

Week 5–6

Tutorials show the happy-path fetch() call. Real apps spend more code on loading and error states than on the actual data display — treating those as an afterthought is why demos feel broken in production.

What you'll learn
  • useEffect for data fetching, and the cleanup function that prevents a real race-condition bug
  • Loading, error, and empty states as first-class UI, not edge cases
  • Why fetching on every render would be a bug, and what dependency arrays actually control
Code
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [status, setStatus] = useState("loading");

  useEffect(() => {
    let cancelled = false;
    fetch("/api/todos")
      .then((r) => r.json())
      .then((data) => { if (!cancelled) { setTodos(data); setStatus("done"); } })
      .catch(() => { if (!cancelled) setStatus("error"); });
    return () => { cancelled = true; }; // prevents setState after unmount
  }, []);

  if (status === "loading") return <p>Loading…</p>;
  if (status === "error") return <p>Something went wrong.</p>;
  return <ul>{todos.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
BuildConnect the Stage 4 todo UI to the real backend API from Stages 1–3 — real loading and error states, not just the happy path.
Self-checkWhy does a useEffect data-fetch need a cleanup function to avoid a real bug, and what bug specifically?
STAGE 06

Connecting the stack — full CRUD end to end

Week 6–7

This is the stage where “I know React” and “I know Express” become “I can build a product” — integration is its own skill, not just the sum of the parts.

What you'll learn
  • Environment variables for API URLs across environments
  • CORS — what it's actually protecting against, and configuring it correctly instead of allow-all
  • Optimistic UI updates, and rolling back cleanly when a request fails
Code
// Correct CORS — allow only your real frontend origin
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));

// Optimistic update — update UI immediately, roll back on failure
async function toggleTodo(id) {
  setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  const res = await fetch(`/api/todos/${id}`, { method: "PATCH" });
  if (!res.ok) {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t)); // revert
  }
}
BuildThe full CRUD todo app, working end to end locally — React frontend, Express/MongoDB backend, JWT auth, all connected.
Self-checkWhat is CORS actually protecting against, and why does setting Access-Control-Allow-Origin: * defeat that protection for a login-protected API?
STAGE 07

Deployment — getting it actually live

Week 7

A MERN app that only runs on localhost isn't a portfolio piece. Deployment has its own gotchas — env vars, build steps, production CORS — that “it works on my machine” never surfaces.

What you'll learn
  • Deploying the Express API to a free host (Render/Railway)
  • Deploying the React frontend (Vercel/Netlify) and wiring the production API URL
  • Connecting a hosted MongoDB (Atlas free tier) instead of a local database
Code
// .env (never committed) vs. .env.example (committed, documents what's needed)
// .env.example
MONGODB_URI=
JWT_SECRET=
FRONTEND_URL=

// Read once, fail loudly if missing — don't silently fall back in production
if (!process.env.MONGODB_URI) {
  throw new Error("Missing MONGODB_URI");
}
BuildDeploy the full Stage 6 app — a real live URL your frontend and backend both actually run at, not localhost.
Self-checkYour app works locally but the deployed frontend can't reach the deployed backend. What are the most likely causes, in order of likelihood?
STAGE 08

Production concerns — security, rate limiting, validation

Week 8

The gap between “a working demo” and “something you'd trust with real user data” is a specific, learnable checklist — not a vague feeling of needing more polish.

What you'll learn
  • Rate limiting — why it matters, and a basic implementation
  • Common HTTP security headers (helmet.js) and what each actually prevents
  • Server-side input validation — why client-side validation alone is not security
Code
// Basic rate limiting — cap requests per IP per window
const attempts = new Map();
function rateLimit(req, res, next) {
  const ip = req.ip;
  const count = (attempts.get(ip) || 0) + 1;
  attempts.set(ip, count);
  setTimeout(() => attempts.set(ip, Math.max(0, (attempts.get(ip) || 1) - 1)), 60_000);
  if (count > 20) return res.status(429).json({ error: "Too many requests" });
  next();
}

// Server-side validation — never trust the client alone
if (!title || title.trim().length === 0 || title.length > 200) {
  return res.status(400).json({ error: "Invalid title" });
}
BuildHarden the deployed app — add rate limiting, real server-side validation (not just frontend), and security headers via helmet.
Self-checkWhy is client-side form validation not actually a security measure, even though it's still worth having?

The capstone: it's already built

Unlike the other tracks, there's no separate capstone project — the app you built across all 9 stages IS the deliverable. Polish it: real auth, real database, deployed, hardened. That live URL is what goes on your resume.

Live URL — frontend and backend both actually deployed
Real auth — hashed passwords, JWTs, protected routes
MongoDB Atlas — not a local database
Rate limiting and server-side validation in place
Environment variables — no secrets committed to git
README documenting what it does and how to run it

Common pitfalls

  • Committing .env files. Once a secret hits git history, rotating it is the only real fix — .gitignore it from commit one.
  • Trusting the frontend. Every check your React form does, your Express API must do again — the client is not a security boundary.
  • Skipping error states. A UI with no loading/error handling isn't half-finished, it's a different (broken) app under real network conditions.
  • Deploying once and stopping. Most real bugs only appear in production — CORS, env vars, cold starts. Deploy early, not at the end.

Resources worth your time

  • Express docsreference
  • MongoDB Atlasfree hosting
  • Mongoose docsreference
  • React docs (react.dev)reference
  • MDN: HTTP & CORSdeep dive