# Express From Zero

> Learn the minimalist web framework that defined Node.js backends: routing, the middleware chain that is Express's whole personality, the request and response objects, building a REST API, error handling, serving and structuring an app, and testing and production. Small core, middleware for everything else.


---

# Express From Zero

Express is the framework that defined what a Node.js backend looks like. It's deliberately tiny - a thin
layer over Node's built-in HTTP server - and that minimalism is its whole identity: Express gives you
routing and a middleware system, and leaves everything else (body parsing, auth, validation, templating)
to middleware you add. A decade-plus of Node tutorials, jobs, and production apps run on it, so even as
newer frameworks appear, Express is the one you're most likely to meet and the clearest lens on how Node
web servers work.

The mental model is one idea repeated everywhere: **the middleware chain**. A request enters and flows
through an ordered series of functions, each with the shape `(req, res, next)`. Each one can read or change
the request, send a response, or call `next()` to pass control to the next function. Routes are just
middleware bound to a method and path; error handlers are middleware with an extra argument. Hold "an
Express app is a pipeline of `(req, res, next)` functions," and the entire framework - routing, parsing,
auth, errors - is the same shape in different costumes.

> 📝 This teaches the **framework** - it assumes you know **JavaScript**: functions, callbacks, promises,
> `async`/`await`, and modules ([JavaScript From Zero](/guides/javascript-from-zero)). It pairs with
> [What a Framework Even Is](/guides/what-a-framework-even-is), and the
> [node:http roots guide](/guides/build-a-server-with-node-http) shows exactly what Express wraps. Compare
> it with [Fastify](/guides/fastify-from-zero) and [NestJS](/guides/nestjs-from-zero). Express runs on
> Node, so examples are shown with the commands to run them.

## How to read this

Read in order - it grows one service (a small **tasks API**) from a single route to a structured, tested,
deployable REST API. Phases carry difficulty badges.

## The phases

**Part 1 - The core (🟢 Basic)**
1. **[What Express Is & Your First Server](01-what-express-is.md)** 🟢 - the app, a route, and a running server in a few lines.
2. **[Routing](02-routing.md)** 🟢 - methods, route params, query strings, and routers.
3. **[Middleware](03-middleware.md)** 🟡 - the `(req, res, next)` chain, ordering, and built-in + third-party middleware.

**Part 2 - A real API (🟡 → 🔴)**
4. **[Request & Response](04-request-and-response.md)** 🟡 - reading the body/params, `res.json`/status, and validation.
5. **[Building a REST API](05-building-a-rest-api.md)** 🟡 - full CRUD wired through routes and middleware.
6. **[Error Handling](06-error-handling.md)** 🔴 - the error-handling middleware, async errors, and one consistent shape.

**Part 3 - Ship it (🟡 → 🟢)**
7. **[Serving & Structuring an App](07-serving-and-structure.md)** 🟡 - static files, structure beyond one file, and config.
8. **[Testing & Production](08-testing-and-production.md)** 🟡 - supertest, environment config, and deployment.
9. **[Where to Go Next](09-where-to-go-next.md)** 🟢 - Express vs Fastify/NestJS, the ecosystem, and what to build.

> The throughline: an Express app is **a chain of `(req, res, next)` functions** - routes, parsers, auth,
> and error handlers are all that one shape. Hold it and Express is a small tool you fully understand.


---

# What Express Is & Your First Server

You know [JavaScript](/guides/javascript-from-zero) and want to put something on the web - an API a
phone app or frontend can talk to. You could build it on Node's raw HTTP server ([that guide](/guides/build-a-server-with-node-http)
is worth seeing once), but you'd rewrite the same plumbing every time: matching URLs, parsing bodies,
sending JSON. That plumbing, packaged into something small and well-worn, is **Express**.

Express is **deliberately tiny** - a thin layer over `node:http`. It gives you two things: a clean way
to **route** requests, and a **middleware** system for everything else. Body parsing, auth, logging,
validation - none of that is built in; you add it as middleware when you need it. That minimalism is
why over a decade of Node jobs, tutorials, and production servers run on it. It's the framework you're
most likely to meet.

📝 **Express** - the dominant minimalist web framework for Node.js. Small core (routing + middleware),
everything else bolted on. It sits on top of Node's HTTP server rather than replacing it. If you've read
[What a Framework Even Is](/guides/what-a-framework-even-is), Express is the textbook case: small and
unopinionated, staying out of your way.

## The mental model: a pipeline of functions

💡 **An Express app is a pipeline of `(req, res, next)` functions, and a route is one of them bound to
a method and a path.** A request flows through an ordered chain; each function can read the request,
change it, send a response, or call `next()` to pass it along. Routing, body parsing, auth, error
handling - all the same shape, different costumes. This chain is the heart of Express (full treatment in
Phase 3). For now: **request → functions → response.**

A route is the simplest member of that family - a function bound to one HTTP method (`GET`) and one path
(`/`). Let's build one.

## Your first server

One install gets you the framework:

```bash
npm install express
```

*What just happened:* `npm` downloaded Express into `node_modules` and recorded it in `package.json`.
This assumes you've run `npm init -y` first - do that if you haven't.

Now the smallest server that does something. Create `index.js`:

```javascript
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello from Express');
});

app.listen(3000, () => console.log('listening on http://localhost:3000'));
```

*What just happened:* four moves you'll use forever.
- `const app = express();` creates your **application object** - it holds your routes and middleware.
- `app.get('/', handler)` **registers a route**: "when a `GET` request hits `/`, run this handler."
  You never call the handler yourself - Express calls it when a matching request arrives (the
  framework's *"don't call us, we'll call you"* relationship).
- The handler gets `(req, res)` - the **request** and the **response**. `res.send(...)` writes the
  string as the response body and finishes the request, with Express setting sensible headers for you.
- `app.listen(3000, ...)` starts the HTTP server on port 3000; the callback fires once it's up.

Run it with plain Node - Express is just a library, no special CLI:

```bash
node index.js
```

```console
$ node index.js
listening on http://localhost:3000
```

Open a second terminal and hit it:

```bash
curl localhost:3000
```

```console
$ curl localhost:3000
Hello from Express
```

*What just happened:* `curl` sent a `GET /` request, Express matched it to your route, called the
handler, and sent back the string. A working web server in seven lines.

⚠️ **The handler must end the request - exactly once.** Every request needs a response. If your handler
never calls `res.send` (or `res.json`, `res.end`), the client hangs until it times out. Call `res.send`
twice and Node throws `ERR_HTTP_HEADERS_SENT` - you can't send a response that's already been sent. One
request, one response.

> 📝 **CommonJS vs ESM.** The example uses `require` (CommonJS), the long-standing Node default. If your
> `package.json` has `"type": "module"`, use `import express from 'express';` instead - everything else
> is identical. We stick with `require` here so examples run on any setup.

## Sending JSON, not just text

A string is fine for "hello world," but real backends speak JSON. Add a second route:

```javascript
app.get('/', (req, res) => {
  res.send('Hello from Express');
});

app.get('/health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});
```

*What just happened:* `res.json(obj)` serializes an object to JSON **and** sets
`Content-Type: application/json` automatically - that header is the real difference from
`res.send(JSON.stringify(obj))`, which leaves you to set it yourself. `res.send` is the generalist
(strings, Buffers, even objects, guessing the type); reach for `res.json` whenever you mean JSON.

Restart the server (`Ctrl+C`, then `node index.js` - no auto-reload yet, that's a later phase) and
check the new route:

```console
$ curl localhost:3000/health
{"status":"ok","uptime":4.21}
```

*What just happened:* the request matched the second route, Express called its handler, serialized the
object, and sent it back with the JSON content type. Add a hundred routes and it's this idea a hundred
times.

## The running example: a tasks API

Across this guide we'll grow one real service so each concept lands on something concrete. Meet the
**tasks API** - a small to-do backend where each task looks like this:

```javascript
const tasks = [
  { id: 1, title: 'Learn Express routing', done: false },
  { id: 2, title: 'Understand middleware', done: false },
];

app.get('/tasks', (req, res) => {
  res.json(tasks);
});
```

*What just happened:* `tasks` is an in-memory array of objects. `GET /tasks` returns it as JSON - the
first endpoint of an API we'll grow into full create/read/update/delete (CRUD). In-memory means data
resets on restart; fine for learning, and we'll cover real storage later.

```console
$ curl localhost:3000/tasks
[{"id":1,"title":"Learn Express routing","done":false},{"id":2,"title":"Understand middleware","done":false}]
```

You've now seen the entire shape of an Express endpoint - method, path, handler, response. Next: routes
that carry data (a task's `id` in the URL, query strings, separate router files) - **Phase 2: Routing**.

## Recap

1. **Express is the minimalist Node.js web framework** - a thin layer over `node:http`. Small core
   (routing + middleware), everything else added on. Install with `npm install express`.
2. The big idea: **an Express app is a pipeline of `(req, res, next)` functions, and a route is one
   bound to a method + path.** Routing, parsing, auth, and errors are all that same shape (Phase 3).
3. A first server is four moves: `express()` creates the app; `app.get(path, handler)` registers a
   route; the handler gets `(req, res)`; `app.listen(port)` starts it. Run with plain `node index.js`.
4. **`res.send` vs `res.json`:** `res.json(obj)` serializes an object *and* sets
   `Content-Type: application/json`. Use it whenever you mean JSON.
5. ⚠️ Every request needs **exactly one** response. Skip it and the client hangs; respond twice and
   Node throws `ERR_HTTP_HEADERS_SENT`.
6. Our running example is a **tasks API** (`{ id, title, done }`), starting from `GET /tasks` and
   growing into full CRUD across the guide.

## Quick check

Three questions on what has to stick - what Express is, how a first server is wired, and how to
return JSON:

```quiz
[
  {
    "q": "What is Express, in one line?",
    "choices": [
      "A minimalist web framework that's a thin layer over Node's built-in http module, giving you routing and a middleware system",
      "A standalone web server written in C that replaces Node entirely",
      "A database for storing JSON documents in Node apps",
      "A frontend UI library for building components in the browser"
    ],
    "answer": 0,
    "explain": "Express is the dominant minimalist Node.js web framework. It doesn't replace Node's HTTP server - it sits on top of node:http and adds clean routing plus a middleware system, leaving everything else to middleware you add."
  },
  {
    "q": "In `app.get('/', (req, res) => { ... })`, what does this line do and who calls the handler?",
    "choices": [
      "It registers a route for GET requests to '/', and Express calls the handler when a matching request arrives",
      "It immediately runs the handler once and caches the result",
      "It sends a GET request to '/' and returns the response",
      "It defines a route but you must call the handler yourself in app.listen"
    ],
    "answer": 0,
    "explain": "app.get(path, handler) registers a route. You never call the handler yourself - Express matches incoming requests by method and path and calls the handler for you. That's the framework's 'don't call us, we'll call you' relationship."
  },
  {
    "q": "What does `res.json(obj)` do that `res.send(JSON.stringify(obj))` does not?",
    "choices": [
      "It serializes the object to JSON AND automatically sets the Content-Type header to application/json",
      "It saves the object to a database before responding",
      "It validates that the object matches a schema",
      "Nothing - they are exactly identical in every way"
    ],
    "answer": 0,
    "explain": "Both produce a JSON string, but res.json also sets Content-Type: application/json automatically, so the client knows it's receiving JSON. With res.send(JSON.stringify(obj)) you'd have to set that header yourself. Reach for res.json whenever you mean JSON."
  }
]
```


---

# Routing

One idea for this whole phase: **a route is a method plus a path, pointing at a handler.** When a
request arrives, Express looks at *what verb* it used (`GET`, `POST`, `DELETE`…) and *what URL path*
it asked for (`/tasks`, `/tasks/42`), then runs the first handler you registered that matches both.
Everything else - params, query strings, routers - is detail layered onto that sentence.

> 📝 Routing answers a different question than the response does. Routing decides *which* function runs;
> the function decides *what* to send back. Keep those two jobs separate in your head and Express stays
> simple.

Two things ride along inside the request, and we'll spend most of this phase on them:

- The **path** can carry variable pieces - `/tasks/42` - that Express hands you as **route params**.
- The **URL** can carry a question-mark **query string** - `/tasks?done=true` - that Express hands you as
  the **query**.

We'll grow the running **tasks API** (each task is `{ id, title, done }`) one route at a time.

## Method routes: one verb, one path

Express gives you a method for each HTTP verb. The shape is always the same: `app.VERB(path, handler)`.

```javascript
const express = require('express')
const app = express()
app.use(express.json()) // lets us read JSON request bodies (more in Phase 3)

let tasks = [
  { id: 1, title: 'Buy milk', done: false },
  { id: 2, title: 'Write guide', done: true },
]

app.get('/tasks', (req, res) => {
  res.json(tasks)
})

app.post('/tasks', (req, res) => {
  const task = { id: tasks.length + 1, title: req.body.title, done: false }
  tasks.push(task)
  res.status(201).json(task)
})

app.listen(3000, () => console.log('listening on http://localhost:3000'))
```

*What just happened:* the **same path** `/tasks` is registered twice, for different verbs. `GET /tasks`
runs the first handler and returns the list; `POST /tasks` runs the second and creates a task. Express
never confuses them - a route is method *and* path, not path alone. The `201` is the HTTP status for
"created"; we set it explicitly because the default would be `200`.

The full set you'll reach for: `app.get`, `app.post`, `app.put`, `app.delete`, `app.patch`. There's also
`app.all(path, handler)`, which matches *every* method for that path - handy for cross-cutting things like
a path-specific guard.

> 💡 You can pass more than one handler: `app.get('/x', mw1, handler)`. Express runs them in order, each
> deciding whether to continue. That's a preview of Phase 3 - under the hood, a route is just middleware
> bound to a method and path.

## Route params: variable pieces of the path

A real API can't register a separate route for task 1, task 2, task 99. Instead you mark a slot in the
path with a colon, and Express fills it in from the actual URL.

```javascript
app.get('/tasks/:id', (req, res) => {
  const id = Number(req.params.id)
  const task = tasks.find((t) => t.id === id)
  if (!task) {
    return res.status(404).json({ error: 'Task not found' })
  }
  res.json(task)
})
```

*What just happened:* `:id` is a **named slot**. A request to `/tasks/2` makes Express set
`req.params.id` to `"2"`, and we look up the matching task. If nothing matches, we send a `404` and
`return` so the rest of the handler doesn't run.

⚠️ **Route params are always strings.** `req.params.id` is `"2"`, not the number `2`. Our tasks have
numeric `id`s, so `tasks.find((t) => t.id === id)` would silently fail with `"2" === 2` being `false` - 
nothing matches, every lookup 404s, and it *looks* like your data is missing. That's why we wrap it in
`Number(req.params.id)`. This bites everyone once; let it bite you here instead of in production.

You can have several params in one path:

```javascript
app.get('/users/:userId/tasks/:taskId', (req, res) => {
  const userId = Number(req.params.userId)
  const taskId = Number(req.params.taskId)
  res.json({ userId, taskId })
})
```

*What just happened:* a request to `/users/7/tasks/3` gives you `req.params.userId === "7"` and
`req.params.taskId === "3"`. Each colon-name becomes its own key on `req.params`. The path reads like the
relationship it models: this task belongs to that user.

## Query strings: optional extras after the `?`

Params live *in* the path and are usually required to identify a thing. **Query strings** live after a `?`
and are for the optional stuff: filtering, sorting, paging. Express parses them into `req.query`.

```javascript
app.get('/tasks', (req, res) => {
  let result = tasks
  if (req.query.done !== undefined) {
    const wantDone = req.query.done === 'true'
    result = result.filter((t) => t.done === wantDone)
  }
  res.json(result)
})
```

*What just happened:* a request to `/tasks?done=true` gives `req.query.done === "true"`. We compare
against the string `'true'` (⚠️ same string-not-boolean trap as params - there's no `true` boolean
hiding in a URL) and filter accordingly. A plain `/tasks` with no query returns everything, because
`req.query.done` is `undefined` and we skip the filter. Multiple params work the same way:
`/tasks?done=true&tag=home` gives you both `req.query.done` and `req.query.tag`.

> 📝 Rule of thumb: **path params identify a resource** (`/tasks/42` - *this* task), **query strings
> shape a collection** (`/tasks?done=true` - *which* tasks). When you're unsure which to use, ask whether
> the value names a thing or filters a list.

## Routers: splitting routes into modules

Pile every route onto `app` and one file balloons fast. `express.Router()` gives you a **mini-app** - 
you define routes on it exactly like you do on `app`, then **mount** it under a path prefix.

```javascript
// routes/tasks.js
const express = require('express')
const router = express.Router()

router.get('/', (req, res) => {
  res.json(tasks) // GET /api/tasks
})

router.get('/:id', (req, res) => {
  res.json({ id: Number(req.params.id) }) // GET /api/tasks/42
})

module.exports = router
```

```javascript
// app.js
const tasksRouter = require('./routes/tasks')
app.use('/api/tasks', tasksRouter)
```

*What just happened:* the router's paths are written **relative to where it's mounted**. Inside the
router, `'/'` and `'/:id'` look like they'd answer the site root, but because we mounted it at
`/api/tasks`, they answer `GET /api/tasks` and `GET /api/tasks/42` - the mount prefix is glued in front
of every route the router defines. This is exactly how you'll split routes across files in Phase 7; a
router *is* an `app` you can carry around and plug in.

## Route order: first match wins

Express checks routes **top to bottom and stops at the first match.** Order is not cosmetic - it changes
behavior.

```javascript
// ⚠️ WRONG ORDER
app.get('/tasks/:id', (req, res) => {
  res.json({ id: req.params.id })
})
app.get('/tasks/done', (req, res) => {
  res.json(tasks.filter((t) => t.done))
})
```

*What just happened:* a request to `/tasks/done` is meant for the second handler - but the first one,
`/tasks/:id`, matches *anything* after `/tasks/`, including the literal word `done`. So `:id` captures
`"done"`, the specific route never runs, and you get `{ "id": "done" }`. The fix is to register the
**specific route before the general one**:

```javascript
// ✅ RIGHT ORDER
app.get('/tasks/done', (req, res) => {
  res.json(tasks.filter((t) => t.done))
})
app.get('/tasks/:id', (req, res) => {
  res.json({ id: req.params.id })
})
```

*What just happened:* now `/tasks/done` hits the exact-match route first and never falls through to the
param route. The guideline that saves you: **specific paths before wildcards, narrow before broad.** The
same logic applies to catch-all "404" routes - they go *last*, because anything above them gets first dibs.

## Recap

- A route is **a method + a path → a handler**. `app.get/post/put/delete/patch`, plus `app.all` for every
  verb on one path.
- **Route params** (`:id`) come through `req.params`, and they are **always strings** - convert with
  `Number(...)` before comparing to numeric data.
- **Query strings** (`?done=true&tag=x`) come through `req.query`; use them for optional filtering and
  sorting. Path params identify a resource; query strings shape a collection.
- `express.Router()` is a mountable mini-app. `app.use('/api/tasks', router)` prefixes every route the
  router defines - this is how you split routes across files.
- Express matches **top to bottom, first match wins.** Put specific routes before wildcards and catch-alls.

Check yourself before moving on:

```quiz
[
  {
    "q": "A request hits GET /tasks/5. What is the value of req.params.id inside the handler?",
    "choices": ["The number 5", "The string \"5\"", "undefined", "An object { id: 5 }"],
    "answer": 1,
    "explain": "Route params are always strings. /tasks/5 gives req.params.id === \"5\", so you must call Number(req.params.id) before comparing to a numeric id."
  },
  {
    "q": "You register app.get('/tasks/:id', ...) and then app.get('/tasks/done', ...) below it. What happens at GET /tasks/done?",
    "choices": ["The /tasks/done handler runs", "The /:id handler runs and captures id = \"done\"", "Express runs both handlers", "Express returns a 404"],
    "answer": 1,
    "explain": "First match wins. /:id matches anything after /tasks/, including \"done\", so it runs first. Put the specific route before the param route."
  },
  {
    "q": "You mount a router with app.use('/api/tasks', router) and the router defines router.get('/:id', ...). Which URL does that route answer?",
    "choices": ["GET /:id", "GET /api/tasks", "GET /api/tasks/:id", "GET /router/:id"],
    "answer": 2,
    "explain": "A router's paths are relative to its mount point. The /api/tasks prefix is glued in front, so router.get('/:id') answers GET /api/tasks/:id."
  }
]
```


---

# Middleware

The secret that makes Express click: there is no separate "middleware feature" bolted on the side.
Middleware *is* Express. Routing is middleware. Body parsing is middleware. Auth, logging, error
handling - all the same shape. Once you see it, the framework stops being a pile of methods to
memorize and becomes one idea you understand all the way down.

## The mental model: a chain of `(req, res, next)` functions

📝 Picture a request walking through a hallway of doors. Each door is a function with three things:
`req` (what came in), `res` (what you'll send back), and `next` (a doorknob that opens the next door).

At each door, the function does one of three things:

1. **Read or modify** `req`/`res`, then call `next()` to pass the request along.
2. **End the response** itself (`res.send(...)`, `res.json(...)`) - the request stops here.
3. **Call `next()`** with nothing changed, just to hand off.

A middleware function looks exactly like this:

```javascript
function myMiddleware(req, res, next) {
  // ...do something with req or res...
  next(); // open the next door
}
```

*What just happened:* three parameters, and a decision about whether to call `next()` - the shape of
every middleware in Express. A route handler like `app.get('/tasks', (req, res) => ...)` is the same
thing; it just happens to be the *last* door, so it sends a response instead of calling `next()`.
Routes are middleware bound to a method and a path.

💡 If you read [Build a Server with node:http](/guides/build-a-server-with-node-http), you already met
`(req, res)` and "call the next function to keep going." Express didn't invent this - it formalized it.

## Writing your first middleware: a request logger

The classic first middleware logs every request: method, URL, status, and duration.

```javascript
const express = require('express');
const app = express();

function logger(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next(); // hand off to the next middleware
}

app.use(logger); // runs for EVERY request

app.get('/tasks', (req, res) => {
  res.json([{ id: 1, title: 'Write the docs' }]);
});

app.listen(3000, () => console.log('http://localhost:3000'));
```

*What just happened:* `app.use(logger)` registers `logger` to run at the start of every request. We
record `start`, then attach a one-time listener to `res`'s `'finish'` event - fired when the response
is fully sent - so we can log the real status code and duration. `logger` calls `next()` immediately;
it doesn't wait for `finish`. Its job in the chain is to step aside and let the request continue. Hit
`GET /tasks` and you'll see `GET /tasks 200 2ms`.

### Three places you can register middleware

```javascript
app.use(logger);                          // GLOBAL: every request
app.use('/admin', requireLogin);          // PATH-SCOPED: only paths under /admin
app.get('/tasks/:id', loadTask, handler); // PER-ROUTE: only this route, in this order
```

*What just happened:* same mechanism, three reaches. `app.use(fn)` runs on everything. `app.use('/admin', fn)`
runs only when the path starts with `/admin`. Listing functions inside a route runs them left to right
for that one route. The middleware doesn't change - only *where you mount it* does.

## ⚠️ Order matters (this is the part that bites everyone)

Middleware runs **in the order you register it**, top to bottom. Getting this wrong is the most
common Express bug there is.

**Trap 1: the parser must come before the routes that need it.** `express.json()` reads the request
body into `req.body`. Register your routes *before* it, and `req.body` is `undefined` there.

```javascript
// ❌ WRONG - route runs before the body is parsed
app.post('/tasks', (req, res) => {
  res.json(req.body); // undefined - nothing parsed it yet
});
app.use(express.json());

// ✅ RIGHT - parse first, then route
app.use(express.json());
app.post('/tasks', (req, res) => {
  res.json(req.body); // { title: "..." } - parsed and ready
});
```

*What just happened:* in the wrong version, the handler runs before `express.json()` has parsed
anything, so `req.body` is `undefined`. In the right version, parsing happens earlier in the chain, so
by the time your handler runs, `req.body` is already populated. Order on the page = order at runtime.

**Trap 2: forgetting `next()` hangs the request.** A middleware that neither responds nor calls
`next()` is a closed door with no knob - the request stands there forever, until the client times out.

```javascript
// ❌ This middleware silently hangs every request
app.use((req, res, next) => {
  console.log('checking request...');
  // no next(), no res.send() - DEAD END
});
```

*What just happened:* this is the number-one beginner bug. The function logs and returns, but never
calls `next()` or sends a response, so Express has no way to know it's done. Every middleware must do
exactly one of two things: **respond**, or **call `next()`**. If a request hangs for no reason, check
for a missing `next()` first.

## Built-in and third-party middleware

You rarely write parsers and security headers yourself - you reach for middleware that already exists.

**Built into Express:**

- `express.json()` - parses JSON request bodies into `req.body`.
- `express.urlencoded({ extended: true })` - parses HTML form submissions into `req.body`.
- `express.static('public')` - serves files from a folder (images, CSS, the front end).

**Popular third-party packages** (install with npm, then `app.use(...)` them):

- `cors` - adds the headers browsers need to allow cross-origin requests.
- `morgan` - a polished request logger (like ours, but configurable).
- `helmet` - sets a bundle of security-related HTTP headers.

```javascript
const cors = require('cors');
const helmet = require('helmet');

app.use(helmet());          // security headers on every response
app.use(cors());            // allow cross-origin requests
app.use(express.json());    // parse JSON bodies
app.use(morgan('dev'));     // log requests
```

*What just happened:* each line plugs a ready-made middleware into the chain, listed near the top so
they run early - security headers and CORS should apply to every response, and `express.json()` must
come before any route reading `req.body` (Trap 1). Helmet, cors, json, a logger: the boring, sensible
opening for most real Express apps.

### Passing data down the chain with `req`

Middleware can **attach things to `req`**, and every later function in the chain can read them. The
textbook case is authentication: one middleware checks who's calling and stashes the user on `req`.

```javascript
function requireAuth(req, res, next) {
  const token = req.headers['authorization'];
  if (!token) {
    return res.status(401).json({ error: 'Not authenticated' });
    // note: we DON'T call next() - the request stops here
  }
  req.user = { id: 1, name: 'Ada' }; // in real life: verify the token
  next(); // authenticated - continue to the route
}

app.use(express.json());
app.use(requireAuth);

app.get('/tasks', (req, res) => {
  res.json({ owner: req.user.name, tasks: [] }); // req.user came from the middleware
});
```

*What just happened:* no header means an unauthenticated request, so `requireAuth` responds `401` and
**stops** - note there's no `next()` after it. A header sets `req.user` and calls `next()`, so by the
time `/tasks` runs, `req.user` is populated and the route trusts that auth already happened upstream.
Two halves of the pattern: **reject and stop**, or **enrich `req` and continue**. The `return` matters
 - without it, the code would respond *and* fall through to `next()`, trying to send two responses.

## Recap

- **Middleware is a `(req, res, next)` function in a chain.** It can read/modify `req`/`res`, end the
  response, or call `next()`. Routes are middleware bound to a method and path.
- Register at three scopes: **global** (`app.use(fn)`), **path-scoped** (`app.use('/admin', fn)`), or
  **per-route** (`app.get(path, mw, handler)`).
- **Order matters.** Middleware runs top to bottom. `express.json()` must come before any route that
  reads `req.body`.
- A middleware that neither responds nor calls `next()` **hangs the request** - the #1 beginner bug.
- Use built-ins (`express.json`, `express.urlencoded`, `express.static`) and third-party packages
  (`cors`, `morgan`, `helmet`) instead of writing your own.
- Share work down the chain by **attaching to `req`** (e.g. an auth middleware sets `req.user`); reject
  with `res.status(401)` and *don't* call `next()`.

## Quick check

```quiz
[
  {
    "q": "What are the three parameters of a standard Express middleware function?",
    "choices": ["req, res, done", "request, response, callback", "req, res, next", "app, req, res"],
    "answer": 2,
    "explain": "Standard middleware is (req, res, next): read/modify req and res, then call next() to pass control along."
  },
  {
    "q": "You register your POST route before express.json(). What happens to req.body in that route?",
    "choices": ["It contains the parsed JSON", "It is undefined because nothing parsed the body yet", "It throws an error", "Express auto-reorders the middleware for you"],
    "answer": 1,
    "explain": "Middleware runs in registration order. If express.json() is registered after the route, the route runs first and req.body is undefined."
  },
  {
    "q": "A middleware logs a message but never calls next() and never sends a response. What is the result?",
    "choices": ["The next middleware runs anyway", "Express sends an automatic 404", "The request hangs until it times out", "The response is sent with status 200"],
    "answer": 2,
    "explain": "Every middleware must either respond or call next(). Doing neither leaves the request with nowhere to go, so it hangs - the most common beginner bug."
  }
]
```


---

# Request & Response

The whole job of a route handler, stripped of ceremony: **it reads from `req` and writes through
`res`.** Input arrives on the request object - URL params, query string, parsed body, headers. You do
something with it, then reach for the response object and send exactly one answer back: a status code
and usually some JSON.

A handler is a small machine with one input port (`req`) and one output port (`res`). Everything in
this phase is just more knobs on those two ports.

> 📝 One thing that trips up everyone once: `req.body` does **not** exist by default. It only gets
> populated if a body-parsing middleware ran first - `express.json()` from [Phase 3: Middleware](03-middleware.md).
> No parser, no `req.body`. Hold that thought; we'll hit it again with code.

## Reading the request

The `req` object carries four sources of input. You'll use all four constantly, so let's name them
plainly.

```javascript
import express from 'express';

const app = express();
app.use(express.json()); // so req.body works for JSON requests

app.post('/tasks/:id', (req, res) => {
  console.log(req.params);  // { id: '42' }  ← from the URL path
  console.log(req.query);   // { sort: 'date' } ← from ?sort=date
  console.log(req.body);    // { title: 'Buy milk' } ← parsed JSON body
  console.log(req.method);  // 'POST'
  console.log(req.path);    // '/tasks/42'
  console.log(req.get('Authorization')); // 'Bearer abc...' ← a header

  res.json({ ok: true });
});

app.listen(3000, () => console.log('http://localhost:3000'));
```

*What just happened:* the same request handed us input through four doors. `req.params` holds the
named pieces of the route pattern (`:id` became `'42'`). `req.query` holds everything after the `?`.
`req.body` holds the parsed body - **but only because `express.json()` ran first**. `req.headers` is
the raw header object; `req.get('Authorization')` is the case-insensitive, one-header shortcut. Note
`req.params.id` is the string `'42'`, not the number `42` - everything from `params` and `query` is
text. Convert when you need a number.

> ⚠️ If you forget `app.use(express.json())` and then read `req.body`, you won't get an error - you'll
> get `undefined`. That silent `undefined` is the single most common "why is my POST broken" moment in
> Express. When `req.body` is empty and you swear you sent a body, check the parser first.

## Writing the response

Now the output port. The `res` object is how you reply, and the methods you'll live in are small.

```javascript
app.get('/tasks/:id', (req, res) => {
  const task = { id: req.params.id, title: 'Buy milk', done: false };

  res.json(task); // sends JSON, sets Content-Type: application/json
});

app.post('/tasks', (req, res) => {
  const task = { id: '7', title: req.body.title };

  res.status(201).json(task); // chain: set the status, then send the body
});

app.delete('/tasks/:id', (req, res) => {
  // ... delete it ...
  res.sendStatus(204); // 204 No Content - empty body, status only
});
```

*What just happened:* `res.json(obj)` serializes an object to JSON and sets `Content-Type` for you - 
the workhorse. `res.status(code)` sets the status code and **returns `res`**, so you can chain it:
`res.status(201).json(task)`. `res.sendStatus(204)` sends a status with an empty body - perfect for a
successful delete. A few more you'll meet: `res.set('X-Foo', 'bar')` for a custom header, `res.send(...)`
for text/HTML/buffers, `res.redirect(url)` for a redirect.

> ⚠️ **Send exactly one response per request.** Each `req`/`res` pair gets one reply, and once you've
> sent it the headers are flushed. Call `res.json()` (or any send) a second time and Express throws
> `Error: Cannot set headers after they are sent to the client`. This usually happens when you forget a
> `return` after an early response:
>
> ```javascript
> if (!task) {
>   res.status(404).json({ error: 'Not found' });
>   // forgot `return` here ↓ - code keeps running and sends again
> }
> res.json(task); // 💥 headers already sent
> ```
>
> 💡 The fix is a habit: `return res.status(404).json(...)`. Returning the response ends the handler
> right there.

## Choosing correct status codes

The status code is a promise to the client about what happened. Lying with `200 OK` on a failure makes
every consumer of your API guess. Use the codes that match reality:

- **200 OK** - the standard "here's your data" success (a GET that found something).
- **201 Created** - you created a resource (a successful POST). Often paired with the new object in the body.
- **204 No Content** - success, but there's nothing to send back (a DELETE).
- **400 Bad Request** - the client sent something wrong (missing or invalid input). This is *their* fault.
- **404 Not Found** - the thing they asked for doesn't exist.

💡 Rough rule of thumb: `2xx` means "it worked," `4xx` means "you (the client) messed up," `5xx` means
"I (the server) messed up." Reaching for the accurate code costs you nothing and saves whoever calls your
API hours of confusion.

## Never trust the input - validate it

Express has **no built-in validation.** None. It happily hands you whatever the client sent, including
nothing, garbage, or hostile junk. That's not a gap to apologize for - it's the minimalist philosophy.
But it means validation is *your* job, and skipping it is how APIs crash on a missing field or save
nonsense to the database.

The simplest approach is a manual guard at the top of the handler: check what you require, and bail
early with `400` if it's missing.

```javascript
app.post('/tasks', (req, res) => {
  const { title } = req.body ?? {}; // ?? {} guards against body being undefined

  if (typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'title is required and must be a non-empty string' });
  }

  const task = { id: '7', title: title.trim(), done: false };
  res.status(201).json(task);
});
```

*What just happened:* before trusting `title` for anything, we checked it - pulled it from `req.body`
(defaulting to `{}` so a missing body doesn't crash us), confirmed it's a non-empty string, and
`return`ed a `400` with a specific message if not. Only past that guard do we build the task. The early
`return` does double duty: it sends one response and stops the handler.

For one or two fields, a hand-written guard is clear and readable. As rules grow (optional fields,
types, lengths, nested objects), reach for a library: **[express-validator](https://express-validator.github.io/)**
layers validation onto the request, or a schema library like **zod** or **joi** lets you declare the
shape once and validate against it.

> ⚠️ The rule never bends: **never trust client input.** Validate before you read it, save it, or pass
> it anywhere. Anyone can send any bytes to your endpoint - assume someone will.

## Recap

- A handler reads from **`req`** (`params`, `query`, `body`, `headers`/`req.get()`) and writes through **`res`** (status + body).
- `req.body` only exists if a body parser like `express.json()` ran first - otherwise it's `undefined`, silently.
- Reply with `res.json(obj)`, set the code with the chainable `res.status(code).json(obj)`, and use `res.sendStatus(204)` for empty successes.
- Send **exactly one response per request** - a second send throws "Cannot set headers after they are sent." Habitually `return` your responses.
- Pick correct status codes: 201 created, 400 bad input, 404 not found, 204 no content.
- Express has no built-in validation. Guard required input by hand (return 400) or use express-validator/zod - and never trust the client.

## Quick check

```quiz
[
  {
    "q": "You POST JSON to an Express route and read req.body, but it's undefined. What's the most likely cause?",
    "choices": ["The client didn't send a body", "You forgot app.use(express.json()) so no body parser ran", "req.body was renamed to req.data", "Express only parses bodies on GET requests"],
    "answer": 1,
    "explain": "req.body is only populated if a body-parsing middleware like express.json() ran first. Without it, req.body is silently undefined."
  },
  {
    "q": "Which line sends a '201 Created' response with the new task as JSON?",
    "choices": ["res.json(task).status(201)", "res.status(201).json(task)", "res.sendStatus(201, task)", "res.created(task)"],
    "answer": 1,
    "explain": "res.status(code) returns res so it's chainable: set the status first, then send the body with res.json(task)."
  },
  {
    "q": "Your handler validates input and returns a 400 if a field is missing, but you still get 'Cannot set headers after they are sent.' What's the fix?",
    "choices": ["Call res.json() twice on purpose", "Use return res.status(400).json(...) so the handler stops after responding", "Add a second express.json() middleware", "Switch the status code to 200"],
    "answer": 1,
    "explain": "Without return, code keeps running after the early response and sends a second one. Returning the response ends the handler so only one response is sent."
  }
]
```


---

# Building a REST API

This is the payoff phase. Everything you've built so far - routing in Phase 2, the
middleware chain in Phase 3, reading the body and validating it in
[Phase 4](04-request-and-response.md) - clicks together into one working API.

## The mental model: five handlers over one collection

A **REST resource** is a collection of things - tasks, users, orders, anything - 
and almost every operation you'll do on a collection is one of five:

| You want to… | HTTP method + path | Express handler |
|--------------|--------------------|-----------------|
| **List** them all | `GET /api/tasks` | `res.json(tasks)` |
| **Get** one by id | `GET /api/tasks/:id` | find, or 404 |
| **Create** a new one | `POST /api/tasks` | validate, add, 201 |
| **Update** one | `PUT /api/tasks/:id` | find + replace, or 404 |
| **Delete** one | `DELETE /api/tasks/:id` | remove, 204, or 404 |

That's it. List, get, create, update, delete. This is not an Express idea - it's
how REST works in every framework, in every language. What changes from framework
to framework is only the *costume*: in Django it's a `ViewSet`, in Rails a
`controller`, in Express it's **five route handlers wired onto a Router**.

> 💡 If you internalize "a resource = these five handlers," learning any new web
> framework becomes a game of "where do they put the five?" The concept transfers;
> only the syntax is new.

We'll build the running **tasks API** - the same little service this guide has been
growing all along.

## The setup: a Router, `express.json()`, and a store

Three pieces before the handlers. First, mount a **Router** for the resource - a
mini-app you attach all the task routes to, then plug into the main app under one
path. Second, add `express.json()` so incoming JSON bodies actually get parsed.
Third, a place to keep the data.

```javascript
const express = require('express');
const app = express();

app.use(express.json()); // parse JSON request bodies into req.body

const router = express.Router();

// In-memory store - a stand-in for a database
let tasks = [];
let nextId = 1;

app.use('/api/tasks', router); // every router route lives under /api/tasks
app.listen(3000, () => console.log('Tasks API on http://localhost:3000'));
```

*What just happened:* `express.Router()` gives us an isolated bundle of routes.
`app.use('/api/tasks', router)` mounts it so `'/'` on the router answers at
`/api/tasks`, and `'/:id'` answers at `/api/tasks/:id`. `app.use(express.json())`
is doing real work - without it, `req.body` would be `undefined` and every
create/update would silently fail. The store is two variables: an array of tasks
and a counter for unique ids.

> 📝 **Why a plain array is safe here.** Node runs your JavaScript on a single
> thread per process, so two requests never mutate `tasks` *at the same instant* - 
> there's no torn read, no lost update, no need for locks. That's a genuine
> convenience for a demo. It is **not** a substitute for a database: the array
> vanishes when the process restarts, and it doesn't survive across multiple
> processes if you scale out. Treat it as scaffolding.

## The five handlers

Now the heart of it. We define all five on the router. Watch how each one maps to a
row in that table above - and how create and update reuse the validation idea from
Phase 4.

```javascript
// LIST - GET /api/tasks
router.get('/', (req, res) => {
  res.json(tasks);
});

// GET ONE - GET /api/tasks/:id
router.get('/:id', (req, res) => {
  const task = tasks.find((t) => t.id === Number(req.params.id));
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  res.json(task);
});

// CREATE - POST /api/tasks
router.post('/', (req, res) => {
  const { title } = req.body;
  if (typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'title is required' });
  }
  const task = { id: nextId++, title: title.trim(), done: false };
  tasks.push(task);
  res.status(201).json(task);
});

// UPDATE - PUT /api/tasks/:id
router.put('/:id', (req, res) => {
  const task = tasks.find((t) => t.id === Number(req.params.id));
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  const { title, done } = req.body;
  if (title !== undefined) {
    if (typeof title !== 'string' || title.trim() === '') {
      return res.status(400).json({ error: 'title must be a non-empty string' });
    }
    task.title = title.trim();
  }
  if (done !== undefined) {
    task.done = Boolean(done);
  }
  res.json(task);
});

// DELETE - DELETE /api/tasks/:id
router.delete('/:id', (req, res) => {
  const index = tasks.findIndex((t) => t.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'Task not found' });
  }
  tasks.splice(index, 1);
  res.sendStatus(204);
});
```

*What just happened:* each handler is small and does one job. A few details earn
their keep:

- **`Number(req.params.id)`** - route params always arrive as *strings*. The store
  uses numeric ids, so `'3' === 3` would be `false` and every lookup would miss.
  Coercing once at the top fixes it.
- **`return res.status(...)`** - the `return` matters. Without it, the handler keeps
  running after sending the 404 and tries to send a second response, which throws
  "Cannot set headers after they are sent."
- **The status codes are the API's vocabulary.** `200` (the default for `res.json`)
  means "here it is." `201 Created` means "I made it, here's the new thing." `204
  No Content` means "done, nothing to send back" - which is why delete uses
  `res.sendStatus(204)` instead of `res.json(...)`. `400` means "your input was
  bad," `404` means "no such thing."
- **Create and update validate before touching the store.** That's the Phase 4
  habit: check the body, reject early with `400`, only then mutate.

## Trying it out

With the server running, drive it from another terminal with `curl`. Walk down the
list and you'll see every status code from the table.

```bash
# Create one
curl -s -X POST http://localhost:3000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Write the README"}'
# → 201  {"id":1,"title":"Write the README","done":false}

# List them
curl -s http://localhost:3000/api/tasks
# → 200  [{"id":1,"title":"Write the README","done":false}]

# Mark it done
curl -s -X PUT http://localhost:3000/api/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"done":true}'
# → 200  {"id":1,"title":"Write the README","done":true}

# Ask for one that doesn't exist
curl -s -i http://localhost:3000/api/tasks/999
# → HTTP/1.1 404 Not Found
#   {"error":"Task not found"}

# Delete it
curl -s -i -X DELETE http://localhost:3000/api/tasks/1
# → HTTP/1.1 204 No Content
```

*What just happened:* you exercised all five handlers and saw the four status codes
the API speaks. The `-i` flag prints the response headers, which is how you confirm
the `404` and `204` - a `204` has an *empty body* by design, so the status line is
the only signal you get. The `-H "Content-Type: application/json"` header is what
tells `express.json()` to parse the body; drop it and `req.body` comes back empty.

## The array is a placeholder - and the errors are repetitive

Two plain observations about what you just built.

💡 **The in-memory array is a database stand-in.** The whole point of keeping the
store behind `tasks.find(...)`, `tasks.push(...)`, and `tasks.splice(...)` is that
the *handlers don't care what's underneath*. When you swap the array for a real
database - through an ORM like Prisma or TypeORM, or raw SQL - the handler shapes
barely change: `tasks.find(...)` becomes `await db.task.findUnique(...)`, and the
`201`/`404`/`204` logic stays exactly as it is. If the concept is fuzzy, the
[how an ORM works](/guides/how-an-orm-works) guide explains the layer that sits
between your handlers and the database.

⚠️ **Notice how repetitive the error handling already is.** Look back: the
`if (!task) return res.status(404)...` block appears in three different handlers,
word for word. The validation `400`s repeat too. Right now each handler is its own
little island of error logic. That works, but it doesn't scale - by the time you
have ten resources you'll have copy-pasted that 404 fifty times, and an unhandled
exception in any handler would crash the process. [Phase 6](06-error-handling.md)
fixes this properly: one centralized error-handling middleware that every handler
delegates to, so the five handlers go back to describing only the *happy path*.

## Recap

- A REST resource is **five handlers over one collection**: list, get, create,
  update, delete. The concept is universal; Express just expresses it as routes on a
  Router.
- The setup is three pieces: `express.Router()` for the resource, `app.use(express.json())`
  to parse bodies, and a store (here, an in-memory array - fine for a demo, not for production).
- Status codes are the API's vocabulary: `200` (here it is), `201` (created),
  `204` (done, no body), `400` (bad input), `404` (not found).
- Coerce `req.params.id` to a `Number`, and always `return` after sending a
  response so a handler doesn't try to respond twice.
- The array is a database stand-in - handler shapes survive the swap to a real
  DB/ORM. See [how an ORM works](/guides/how-an-orm-works).
- The duplicated `404` and `400` logic is a smell; Phase 6 centralizes it.

## Quick check

```quiz
[
  {
    "q": "Why coerce req.params.id with Number() before comparing it to a task's id?",
    "choices": ["To make the URL shorter", "Route params arrive as strings, so '3' === 3 is false and the lookup would always miss", "Express requires all ids to be numbers", "It prevents SQL injection"],
    "answer": 1,
    "explain": "Route params are always strings. Without coercion, comparing the string '3' to the numeric id 3 is false, so every find() misses."
  },
  {
    "q": "Which status code should a successful DELETE that returns no body use?",
    "choices": ["200 OK", "201 Created", "204 No Content", "404 Not Found"],
    "answer": 2,
    "explain": "204 No Content means the action succeeded and there's nothing to send back - which is why delete uses res.sendStatus(204)."
  },
  {
    "q": "What is the in-memory tasks array meant to represent in a real application?",
    "choices": ["A permanent storage solution", "A cache layer in front of Redis", "A stand-in for a database that you swap for a real DB/ORM later", "A required part of every Express app"],
    "answer": 2,
    "explain": "The array is scaffolding. The handlers are written so that swapping it for a real database (often via an ORM) leaves their shape mostly unchanged."
  }
]
```


---

# Error Handling

In [Phase 5](05-building-a-rest-api.md) every handler sprinkled its own
`res.status(404).json({ error: 'Not found' })` and its own `try`/`catch`. Each route reinvented "what
does an error look like to the client?" - and they didn't all agree.

The better way is an idea you already know: **errors are routed to a special piece of middleware.**
You don't handle errors where they happen - you hand them off, and one function at the end of the
chain decides what the client sees.

## The mental model: an error is routed to a special door

📝 Remember the hallway of doors from [Phase 3](03-middleware.md)? Normal middleware has the shape
`(req, res, next)`. Express has one more kind of door - an error handler - with a four-argument shape:
`(err, req, res, next)`. That extra first parameter is the whole signal: Express counts your function's
parameters, and four means "this is the error door," skipped during normal traffic.

A request reaches the error door two ways:

1. You **call `next(err)`** with an argument - Express stops the normal chain and jumps to the error handler.
2. You **throw in synchronous code** - Express catches the throw and does the same jump.

So the rule: anywhere something goes wrong, don't respond - call `next(err)` (or throw). One handler,
one consistent error response, everywhere.

```javascript
const express = require('express');
const app = express();
app.use(express.json());

app.get('/tasks/:id', (req, res, next) => {
  const task = findTask(req.params.id);
  if (!task) {
    return next(new Error('Task not found')); // hand off - don't respond here
  }
  res.json(task);
});
```

*What just happened:* the handler doesn't build a 404 itself - it creates an `Error` and passes it to
`next()`. Because `next` received an argument, Express abandons the normal chain and looks for an
error-handling middleware. The handler's job ends at "something is wrong, here's what."

## The error-handling middleware (and a custom error that carries its status)

The error door must be registered **last**, after every route, so it catches whatever gets sent its
way. A plain `new Error('Task not found')` has a message but no notion of "this should be a 404" - fix
that with a tiny custom error class that carries a status code.

```javascript
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

// ...routes go here...

// LAST: the one error handler for the whole app
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({ error: err.message || 'Internal Server Error' });
});
```

*What just happened:* `AppError` is a normal `Error` with one extra field, `statusCode`. A route can
now throw `new AppError('Task not found', 404)` and the handler reads `err.statusCode` to set the
response code. Anything without one - a real bug, a thrown string, a library blowing up - falls through
to `500`, your safety net against leaking a stack trace. Note the handler keeps all four parameters;
that signature is the only thing marking it as the error door, so keep `next` even unused.

⚠️ Order is everything (Phase 3's Trap 1, again). The error handler goes **after** all your routes.
Register it early and it sits in front of routes that never produce errors during normal flow - useless
 - while the real errors at the end have nowhere to land.

## ⚠️ The async-error trap (this one bites everyone)

The "throw and Express catches it" magic **only works for synchronous code.** Watch an `async` handler
on **Express 4**:

```javascript
// ⚠️ EXPRESS 4: this error vanishes - it never reaches your handler
app.get('/tasks/:id', async (req, res) => {
  const task = await db.findTask(req.params.id); // if this rejects...
  if (!task) throw new AppError('Task not found', 404); // ...or this throws
  res.json(task);
});
```

*What just happened:* when an `async` function throws (or an `await`ed promise rejects), it doesn't
throw *synchronously* - it returns a **rejected promise**. Express 4 never looks at that promise, so
the rejection floats off as an unhandled promise rejection. Your error handler is never called, the
request hangs until it times out, and your terminal prints a scary warning. The error went nowhere.

Three ways out, in order of how much you should reach for them:

**Option A - `try`/`catch` and call `next(err)` by hand.** Explicit, no dependencies, but repeated in
every async handler:

```javascript
app.get('/tasks/:id', async (req, res, next) => {
  try {
    const task = await db.findTask(req.params.id);
    if (!task) throw new AppError('Task not found', 404);
    res.json(task);
  } catch (err) {
    next(err); // manually ferry it to the error handler
  }
});
```

*What just happened:* `try`/`catch` turns the async rejection back into something you control. A throw
inside `try` - your `AppError` or a rejected `await` - lands in `catch`, and `next(err)` does the
hand-off Express 4 wouldn't. Correct, but repeating this in twenty routes rots fast.

**Option B - wrap once, reuse everywhere.** A tiny higher-order function wraps an async handler and
auto-forwards any rejection:

```javascript
const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/tasks/:id', asyncHandler(async (req, res) => {
  const task = await db.findTask(req.params.id);
  if (!task) throw new AppError('Task not found', 404);
  res.json(task); // no try/catch - the wrapper handles rejections
}));
```

*What just happened:* `asyncHandler` runs `fn`, wraps the result in `Promise.resolve(...)` to guarantee
a promise, and attaches `.catch(next)` - any rejection routes straight to the error door. Handlers go
back to clean linear code with no `try`/`catch`, and every error still lands in one place. (The popular
[`express-async-errors`](03-middleware.md) package does the same globally via a one-line `require`.)

💡 **Express 5 fixes this at the source.** A rejected promise from an `async` handler is forwarded to
your error handler automatically - no wrapper, no `try`/`catch`. Starting fresh, use Express 5 and write
plain `async` handlers. On an existing Express 4 codebase (still extremely common), reach for
`asyncHandler`. Knowing which world you're in is the whole game.

## The 404 catch-all

The error handler covers things that go *wrong*. But a request to a path no route matches - `GET /taks`
with a typo - fires no route, so Express falls through to its bland default HTML 404. For a JSON API
you want a JSON 404, in the same shape as every other error.

The fix is a catch-all middleware placed **after all your routes but before the error handler**:

```javascript
app.use(express.json());

app.get('/tasks/:id', /* ... */);
app.post('/tasks', /* ... */);
// ...all other routes...

// 1) nothing matched above → it's a 404
app.use((req, res) => {
  res.status(404).json({ error: 'Not Found' });
});

// 2) LAST: the error handler (four args)
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({ error: err.message || 'Internal Server Error' });
});
```

*What just happened:* `app.use(...)` with no path matches every request, but registered after all real
routes, it only runs when nothing else responded - an unmatched path. It sits *above* the error handler
because the error handler (four args) is reserved for errors routed via `next(err)`; this catch-all
(three args) handles "nobody answered." Together they cover both dead ends: "doesn't exist" and
"something broke."

## Thin handlers, one central translator

💡 Compare to Phase 5, where each handler did its own `res.status(404)`. Now handlers get to be *thin*:
they do the happy path and **throw a typed error** when reality disagrees, never thinking about status
codes or JSON envelopes.

```javascript
function getTaskOr404(id) {
  const task = db.findTask(id);
  if (!task) throw new AppError('Task not found', 404);
  return task;
}

app.get('/tasks/:id', asyncHandler(async (req, res) => {
  const task = getTaskOr404(req.params.id); // throws AppError(404) if missing
  res.json(task); // only the success case lives here
}));
```

*What just happened:* the "not found" decision moved into a small service function that throws
`AppError('Task not found', 404)`. The route handler reads like a sentence - get the task, send it - 
with the failure path delegated. `asyncHandler` forwards the throw, and the central error handler maps
its `statusCode` and `message` to the response. Every error now flows through one function: one
consistent shape, one place to log, one place to hide stack traces in production.

## Recap

- Errors are **routed to a special four-argument middleware** `(err, req, res, next)`, registered
  **last**. The four-arg signature is the only thing that marks it as the error door.
- Reach it by calling **`next(err)`** or by **throwing in synchronous code** (Express catches sync
  throws automatically).
- A custom **`AppError extends Error`** carrying a `statusCode` lets handlers throw
  `new AppError('not found', 404)`; anything without a `statusCode` falls through to `500`.
- ⚠️ **Async errors are the trap:** Express 4 does **not** catch rejected promises from `async`
  handlers - use `try`/`catch` + `next(err)`, an `asyncHandler` wrapper, or `express-async-errors`.
  **Express 5 forwards them automatically.**
- Add a **404 catch-all** after all routes and **before** the error handler, so unmatched paths return
  JSON in the same shape.
- The payoff: **thin handlers throw typed errors; one central handler translates them to status + JSON.**

## Quick check

```quiz
[
  {
    "q": "What makes Express treat a middleware function as an error handler?",
    "choices": ["A call to app.error() instead of app.use()", "Its four-argument signature (err, req, res, next)", "Registering it before all routes", "Naming the function errorHandler"],
    "answer": 1,
    "explain": "Express identifies the error handler purely by its arity: four parameters (err, req, res, next). It must also be registered last, after all routes."
  },
  {
    "q": "On Express 4, an async route handler does `await db.find()` and the promise rejects. With no try/catch and no wrapper, what happens?",
    "choices": ["Express automatically routes it to the error handler", "The rejection becomes an unhandled promise rejection and the request hangs", "Express sends a 500 with the stack trace", "The 404 catch-all handles it"],
    "answer": 1,
    "explain": "Express 4 ignores the rejected promise an async handler returns, so the error never reaches your handler - the request hangs. Express 5 fixes this; on 4 you need try/catch, an asyncHandler wrapper, or express-async-errors."
  },
  {
    "q": "Where does the JSON 404 catch-all middleware belong relative to the routes and the error handler?",
    "choices": ["Before all routes, so it runs first", "After all routes, but before the error handler", "After the error handler", "It replaces the error handler"],
    "answer": 1,
    "explain": "Placed after all routes, the catch-all only runs when no route matched (an unmatched path). It must sit before the four-arg error handler, which is reserved for actual errors routed via next(err)."
  }
]
```


---

# Serving & Structuring an App

Up to now the tasks API has lived in one file - the easiest thing to read while learning the shapes.
But every real app outgrows a single file, and the way it grows isn't random. There's a standard
Express layout you'll recognize in nearly every Node codebase you open.

## The mental model: split by responsibility

You don't split a growing app by *file size* - you split it by **responsibility**. Each piece answers
one question:

- **Routes** - *which URL maps to which handler?* (the wiring)
- **Controllers** - *how do I read the HTTP request and shape the HTTP response?* (the web layer)
- **Services** - *what's the actual business logic and data access?* (the brains)
- **Middleware** - *what runs in the chain around the handlers?* (auth, error handling)

One more split, easy to miss, that pays off hugely: **building the app** is a different job from
**starting the server**. `app.js` assembles the middleware and routers and exports the app; `server.js`
imports it and calls `app.listen`.

💡 This is the Node echo of MVC. Routes point at controllers, controllers stay thin and delegate to
services, services hold the logic. Keep handlers thin and the logic lands somewhere you can test
without spinning up a web server.

First, a quick win that needs almost no structure: serving files.

## Serving static files

A backend rarely serves only JSON - sooner or later you have a built frontend, images, a PDF, a
favicon, plain files to hand the browser as-is. Express has one line for that.

```javascript
const express = require('express');
const app = express();

app.use(express.static('public'));

app.listen(3000);
```

*What just happened:* `express.static('public')` is built-in middleware that serves everything inside
a `public` folder at the **root** of your site. `public/index.html` answers at `http://localhost:3000/`;
`public/styles.css` answers at `/styles.css`. Images, JS bundles, fonts - all served automatically with
the right headers, no routes written. This is how you'd serve a built React/Svelte/Vue frontend out of
the same Express app that serves your API.

📝 The path is relative to where you *start the process*, not where `express.static` is called from - 
use `path.join(__dirname, 'public')` if you ever run the server from a different directory.

For assets that don't change often, add `{ maxAge: '1d' }` - `app.use(express.static('public', { maxAge: '1d' }))`
sets `Cache-Control: max-age=86400` so browsers hold the files for a day instead of re-fetching every
load. (Files that *do* change typically rely on hashed filenames so a new build gets a new URL.)

## Structure beyond one file

Now the real refactor: spread the tasks API from [Phase 5](05-building-a-rest-api.md) and
[Phase 6](06-error-handling.md) across the standard layout. **Nothing about the behavior changes** - 
same routes, same status codes, same logic. We're only moving code into the box that matches its job.

```
tasks-api/
  app.js                       ← builds the app (middleware + routers), exports it
  server.js                    ← imports app, calls app.listen
  routes/
    tasks.routes.js            ← maps URLs → controller functions
  controllers/
    tasks.controller.js        ← reads req, shapes res (thin)
  services/
    tasks.service.js           ← business logic + the data store
  middleware/
    error-handler.js           ← the centralized error handler from Phase 6
  public/                      ← static files (optional)
```

Let's build it bottom-up - logic first, then the web layer, then the wiring.

### The service: logic and data, no HTTP

The service knows nothing about `req` or `res`. It deals in plain data and throws plain errors - the
whole point is that it's testable without a web server.

```javascript
// services/tasks.service.js
let tasks = [];
let nextId = 1;

function listTasks() {
  return tasks;
}

function getTask(id) {
  return tasks.find((t) => t.id === id);
}

function createTask(title) {
  const task = { id: nextId++, title, done: false };
  tasks.push(task);
  return task;
}

function deleteTask(id) {
  const index = tasks.findIndex((t) => t.id === id);
  if (index === -1) return false;
  tasks.splice(index, 1);
  return true;
}

module.exports = { listTasks, getTask, createTask, deleteTask };
```

*What just happened:* the array and its operations now live behind named functions, with no `res.json`
or status code - the service returns data (`getTask` returns the task or `undefined`) and lets the
caller decide what HTTP means. This is the seam Phase 5 promised: swap this file for one backed by a
real database and the controllers above it don't change.

### The controller: read the request, shape the response

The controller is the *only* layer that touches `req` and `res`. It parses input, calls the service,
and translates the result into HTTP.

```javascript
// controllers/tasks.controller.js
const service = require('../services/tasks.service');

function list(req, res) {
  res.json(service.listTasks());
}

function getOne(req, res) {
  const task = service.getTask(Number(req.params.id));
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  res.json(task);
}

function create(req, res) {
  const { title } = req.body;
  if (typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'title is required' });
  }
  res.status(201).json(service.createTask(title.trim()));
}

function remove(req, res) {
  const deleted = service.deleteTask(Number(req.params.id));
  if (!deleted) {
    return res.status(404).json({ error: 'Task not found' });
  }
  res.sendStatus(204);
}

module.exports = { list, getOne, create, remove };
```

*What just happened:* each function is *thin* - parse → call service → respond. The
`Number(req.params.id)` coercion and `return res.status(...)` discipline from Phase 5 still live here,
since those are genuinely HTTP concerns; the "find it, mutate the array" mechanics moved to the service.
A controller should read like a description of the request/response contract, nothing more.

### The routes: pure wiring

The route file does one job: connect a method and path to a controller function. No logic, no
validation, no `res` - just the map.

```javascript
// routes/tasks.routes.js
const express = require('express');
const controller = require('../controllers/tasks.controller');

const router = express.Router();

router.get('/', controller.list);
router.get('/:id', controller.getOne);
router.post('/', controller.create);
router.delete('/:id', controller.remove);

module.exports = router;
```

*What just happened:* this is the Phase 5 router, stripped to the wiring. You can read the entire
surface of the resource in five lines - exactly what you want coming back in six months to remember
"what URLs does this thing answer?"

### app.js vs server.js: build it, then start it

This is the split that trips people up. `app.js` assembles the application and **exports** it - it does
not call `listen`. `server.js` imports that app and starts listening.

```javascript
// app.js
const express = require('express');
const tasksRoutes = require('./routes/tasks.routes');
const errorHandler = require('./middleware/error-handler');

const app = express();

app.use(express.json());
app.use(express.static('public'));
app.use('/api/tasks', tasksRoutes);

app.use(errorHandler); // error-handling middleware goes LAST

module.exports = app;
```

```javascript
// server.js
const app = require('./app');

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Tasks API on http://localhost:${port}`);
});
```

*What just happened:* `app.js` is now a pure *recipe* for an Express app - middleware, routers, error
handler, in the right order (`express.json()` before routes that need a parsed body; the error handler
dead last, per Phase 6). `server.js` is the one place that actually opens a port.

📝 **Why this split is worth the extra file.** A test wants to fire requests at your app, not squat a
real server on port 3000 (tests run in parallel, ports collide, a left-open server hangs the run).
Because `app.js` exports the app *without* listening, a test can `require('./app')` and hand it to a
tool like supertest, which drives it in-memory - no port, no `listen`, no cleanup. That's exactly what
[Phase 8](08-testing-and-production.md) does.

## Config via the environment

`server.js` used `process.env.PORT` - the start of the last piece: **configuration belongs in the
environment, not in your code.**

The same app runs on your laptop, in CI, and in production, and each needs different settings: a
different port, database URL, API keys. Hard-coding those means editing source on every deploy and,
far worse, committing secrets into git. The fix is `process.env` - Node hands you every environment
variable on that object.

```javascript
const port = process.env.PORT || 3000;
const databaseUrl = process.env.DATABASE_URL;
```

*What just happened:* `process.env.PORT` reads whatever the environment provides; `|| 3000` is a
fallback for local dev. Your host (Render, Railway, Fly, a Docker `ENV`) sets `PORT` and
`DATABASE_URL` in production, and the same code picks them up with zero edits.

Typing `PORT=3000 DATABASE_URL=... node server.js` every time is miserable. In dev, keep those in a
`.env` file and load it. Two ways:

```bash
# Option A - the dotenv package (works on any Node version)
npm install dotenv
```

```javascript
// at the very top of server.js, before anything reads process.env
require('dotenv').config();
```

```bash
# Option B - Node's built-in flag (Node 20.6+, no package needed)
node --env-file=.env server.js
```

*What just happened:* both read a `.env` file like the one below and load each line into `process.env`
before your code runs. `dotenv` is the long-standing package that works everywhere; `--env-file` is the
newer built-in needing no dependency - pick one.

```bash
# .env - values for local development only
PORT=3000
DATABASE_URL=postgres://localhost:5432/tasks_dev
```

⚠️ **Never commit `.env`, and never hard-code secrets.** Add `.env` to `.gitignore` on day one. A real
secret checked into git is checked in *forever* - it lives in history even after you delete it, and
scanners find leaked keys within minutes. Commit a `.env.example` with the *keys* but **fake values**,
so a teammate knows what to set without seeing the real thing. In production, you don't use a `.env`
file at all - set real environment variables through your host's dashboard or secrets manager.

## Why all this splitting pays off

The controllers are thin - they describe the HTTP contract and nothing else. The logic sits in a
service with no web dependencies, so you can test it as plain functions. The routes are a five-line map
of the resource. And `app.js` builds an app that a test can import without ever opening a port.

💡 That's the throughline into the next phase: **the layered split keeps handlers thin and logic
testable - Node's take on MVC.** You restructured so Phase 8 can write real tests against real code
without fighting the framework.

## Recap

- Split a growing app **by responsibility**, not by file length: routes (wiring), controllers (HTTP),
  services (logic + data), middleware (the chain).
- `express.static('public')` serves a folder of files at the site root - perfect for a built frontend
  or assets; add `{ maxAge: '1d' }` to let browsers cache them.
- Separate **building the app** (`app.js`, exports the app, no `listen`) from **starting it**
  (`server.js`, calls `app.listen`) - so tests can import the app without a running server.
- Read config from `process.env` (`PORT`, `DATABASE_URL`); load a `.env` in dev via `dotenv` or Node's
  built-in `--env-file`.
- Never commit `.env` or hard-code secrets - gitignore it, ship a `.env.example` with fake values, and
  set real env vars in production.

## Quick check

```quiz
[
  {
    "q": "Why does app.js export the app instead of calling app.listen() itself?",
    "choices": ["Because express.json() requires it", "So tests can import the fully-built app and drive it without starting a real server on a port", "Because app.listen only works in production", "To make the file shorter"],
    "answer": 1,
    "explain": "Splitting build (app.js) from start (server.js) lets a test require the app and hand it to a tool like supertest in-memory - no port, no listen, no cleanup. That's what Phase 8 relies on."
  },
  {
    "q": "What does express.static('public') do?",
    "choices": ["Caches all API responses for one day", "Serves the files in the 'public' folder at the site root, with correct content types", "Disables dynamic routes", "Validates incoming JSON bodies"],
    "answer": 1,
    "explain": "It's built-in middleware that serves a folder of files (HTML, CSS, JS, images) at the root - public/index.html answers at /, public/styles.css at /styles.css."
  },
  {
    "q": "Where should the database URL and port come from, and how do you handle secrets?",
    "choices": ["Hard-coded in app.js and committed to git", "From process.env, loaded from a .env in dev that is gitignored; set real env vars in production", "Always passed as command-line arguments by hand", "Stored in the public folder so the frontend can read them"],
    "answer": 1,
    "explain": "Config belongs in the environment (process.env). In dev, load a .env via dotenv or --env-file, but gitignore it and never commit secrets; production sets real environment variables through the host."
  }
]
```


---

# Testing & Production

The mental model that makes Express testing click: **a test calls your app in memory - no real
network, no open port.** Hand `supertest` your Express `app` object; it sends a fake request straight
into the middleware chain, your routes run exactly as they would in production, and you get back a
response to assert on. Nothing is "running" in the background.

This is the entire payoff of the `app` / `app.listen` split from [Phase 7](07-serving-and-structure.md).
Because `app.js` exports the configured app *without* starting a listener, a test file can
`require('../app')` and exercise it directly. If `app.js` called `app.listen()` at the bottom,
importing it would try to bind a port every time you ran the suite - slow, flaky, and a mess when ten
test files all want port 3000.

> 💡 **Supertest is a fake browser that lives inside your test process.** It speaks HTTP to your app
> object, not over the wire. That's why tests are fast and need no server.

## Testing the tasks API

You need a **test runner** (finds tests, runs them, reports pass/fail - `jest`, `vitest`, or Node's
built-in `node:test`) and **supertest** (drives HTTP requests at your app). Install the pair:

```bash
npm install --save-dev supertest jest
```

The one line that matters is the import - the app *without* `listen`:

```javascript
const request = require('supertest');
const app = require('../app');   // the configured app, NOT a running server

test('GET /api/tasks returns 200', async () => {
  const res = await request(app).get('/api/tasks');
  expect(res.statusCode).toBe(200);
});
```

*What just happened:* `request(app)` wraps your app in a test client. `.get('/api/tasks')` builds a
fake request and pushes it through the middleware chain - routers, parsers, your handler, all of it.
`await` resolves once your route sends a response; `res` holds the status, headers, and body. No port
opened; this test passes in milliseconds.

Writes work the same way - chain `.send()` for a JSON body, and `.expect()` asserts status inline:

```javascript
test('POST /api/tasks creates a task', async () => {
  const res = await request(app)
    .post('/api/tasks')
    .send({ title: 'write tests' })
    .expect(201);

  expect(res.body.title).toBe('write tests');
});
```

*What just happened:* `.send({ title: 'write tests' })` sets the request body and the
`Content-Type: application/json` header, so your `express.json()` parser populates `req.body` like a
real client would. `.expect(201)` fails the test if the status isn't 201. Then we assert on `res.body`
 - the parsed JSON your route returned.

> 📝 Test the unhappy paths too. A `POST` with a missing `title` should return `400`; a
> `GET /api/tasks/9999` should return `404`. Those are where your Phase 4/6 validation and error
> handling earn their keep - and where regressions hide.

Run the suite with your runner (`npx jest`, or a `"test": "jest"` script). These `request(app)` tests
become your **regression net in CI** - see [Testing in CI](/guides/testing-in-ci) for wiring that up.

> ⚠️ Shared mutable state will bite you. If tasks live in an in-memory array, one test's `POST` leaks
> into the next test's `GET`. Reset state between tests (jest's `beforeEach`) or your suite passes
> alone and fails together - a classic, maddening flake.

## Getting ready for production

A server that runs on your laptop isn't ready for the open internet. Three concerns separate the two:
config that changes per environment, middleware that protects and speeds up the app, and shutdown
behavior that doesn't drop requests.

**Config comes from the environment, never hardcoded.** Port, `NODE_ENV`, database URL - all in
environment variables so the same code runs unchanged on your machine, in CI, and in production.

```javascript
const PORT = process.env.PORT || 3000;
const isProd = process.env.NODE_ENV === 'production';
```

*What just happened:* `process.env.PORT` reads the port your host assigns; `|| 3000` is a local
fallback. `NODE_ENV === 'production'` becomes a switch for behaviors - verbose logging off, stack
traces hidden. Setting `NODE_ENV=production` also makes Express itself skip dev-only work and cache
views, so it's not optional cosmetics.

**Then the production middleware.** Express's tiny core means security and performance are opt-in
packages you stack at the top of the chain:

```javascript
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const rateLimit = require('express-rate-limit');

app.use(helmet());          // sets safe HTTP security headers
app.use(cors());            // controls who may call your API from a browser
app.use(compression());     // gzips responses to shrink payloads
app.use(rateLimit({ windowMs: 60_000, limit: 100 }));  // caps requests per IP
```

*What just happened:* each `app.use` adds one more `(req, res, next)` function to the chain - the same
shape since Phase 3, just from npm. `helmet` sets defensive headers (hiding `X-Powered-By`, content-type
protections). `cors` decides which origins may call your API. `compression` gzips bodies. `rateLimit`
rejects an IP past 100 requests a minute. Put these before your routes so every request passes through.

## Shutting down without dropping requests

When a platform redeploys or scales down, it sends `SIGTERM` and gives your process a few seconds to
exit. Ignore it and in-flight requests get cut off mid-response. **Graceful shutdown means: stop
accepting new connections, let current ones finish, then exit** - the reason you keep the return value
of `app.listen`.

```javascript
const server = app.listen(PORT, () => {
  console.log(`listening on ${PORT}`);
});

function shutdown() {
  console.log('shutting down...');
  server.close(() => process.exit(0));   // drains in-flight requests, then exits
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);          // Ctrl-C in your terminal
```

*What just happened:* `app.listen` returns the underlying Node HTTP server, kept in `server`. On
`SIGTERM` (deploy) or `SIGINT` (Ctrl-C), `server.close()` stops accepting *new* connections but lets
in-flight requests finish; once done, the callback fires and we exit cleanly. No half-written
responses, no abandoned clients.

A few more production realities, briefly:

- **Run it under a supervisor.** A process manager like **PM2**, or a container orchestrator, restarts
  your app if it crashes and runs multiple instances across CPU cores. Don't rely on `node app.js` in a
  terminal staying alive.
- **Sit behind a reverse proxy.** In production your app almost always runs behind nginx (or your
  platform's load balancer), which terminates TLS and forwards requests. Tell Express to trust it so
  `req.ip` and `req.protocol` reflect the real client, not the proxy:

```javascript
app.set('trust proxy', 1);   // honor X-Forwarded-* headers from one proxy hop
```

*What just happened:* without this, every request looks like it came from the proxy's address (often
`127.0.0.1`), quietly breaking rate limiting and IP-based logic. `trust proxy` tells Express to read
`X-Forwarded-For` / `X-Forwarded-Proto`, so `req.ip` is the real visitor and `req.secure` is true for
HTTPS.

> ⚠️ Don't leak stack traces to clients in production. Your Phase 6 error handler should log the full
> error server-side (with **pino** or **winston**) but send the client only a generic message and
> status in prod - a stack trace exposes file paths, dependency versions, sometimes secrets. That's
> what the `isProd` switch is for.

When you're ready to take the whole thing live - domains, TLS, environment secrets, the deploy itself
 - [Ship Your Side Project](/guides/ship-your-side-project) walks the last mile.

## Recap

- **Tests run in memory:** `request(app)` from supertest pushes fake HTTP requests through your
  middleware chain with no port and no network - fast and isolated.
- **The app/listen split makes this possible:** a test imports the configured `app`; only the entry
  point calls `app.listen`. Test happy *and* unhappy paths, and reset shared state between tests.
- **Production config comes from the environment** (`process.env.PORT`, `NODE_ENV=production`), never
  hardcoded.
- **Stack hardening middleware before your routes:** `helmet`, `cors`, `compression`, and a rate limiter.
- **Shut down gracefully:** capture `const server = app.listen(...)` and call `server.close()` on
  `SIGTERM`/`SIGINT` so in-flight requests drain.
- **Behind a proxy, set `app.set('trust proxy', 1)`**, run under PM2 or a container, and never send
  stack traces to clients in prod.

## Quick check

```quiz
[
  {
    "q": "Why can supertest test your Express app without opening a network port?",
    "choices": ["It mocks every route by hand", "It pushes fake requests directly through the imported app's middleware chain in memory", "It starts a hidden server on a random port", "It only works against a deployed URL"],
    "answer": 1,
    "explain": "supertest takes the app object and runs requests through its middleware chain in-process - no socket, no port. This is why the Phase 7 app/listen split matters: you import the app, not a running server."
  },
  {
    "q": "On SIGTERM during a deploy, what does server.close() do?",
    "choices": ["Kills all connections instantly", "Stops accepting new connections but lets in-flight requests finish, then exits", "Restarts the server", "Closes the database only"],
    "answer": 1,
    "explain": "server.close() stops accepting new connections and waits for current requests to complete before its callback fires - so you can exit cleanly without dropping in-flight responses."
  },
  {
    "q": "Why set app.set('trust proxy', 1) when running behind nginx?",
    "choices": ["It enables HTTPS", "It compresses responses", "So req.ip and req.protocol reflect the real client via X-Forwarded-* headers, not the proxy", "It installs helmet automatically"],
    "answer": 2,
    "explain": "Behind a proxy, requests appear to come from the proxy's address. trust proxy tells Express to read the X-Forwarded-* headers, restoring the real client IP and protocol - which rate limiting and IP logic depend on."
  }
]
```


---

# Where to Go Next

Look at what you can actually do now. You can spin up an Express server, route requests by method and
path with params and query strings, order middleware in the `(req, res, next)` chain, shape a response
with the right status code, validate input, build full CRUD for a resource, catch failures in one
error-handling middleware, structure the app past a single file, and test it with supertest before
shipping it with real config. That's a working REST API, not a toy.

And here's the quieter win: because Express is so small, you saw what a framework *is*. Strip the
helpers away and an Express app is one idea repeated everywhere: **a pipeline of `(req, res, next)`
functions over Node's built-in HTTP server.** Routes, parsers, auth, and the error handler are all that
same shape in different costumes. Nothing was hidden behind magic, so when something breaks at 2am, you
can reason about it.

This last phase is the map: where Express sits among the other Node web frameworks, a version note
worth knowing, the ecosystem you'll add next, and one concrete thing to go build.

## Express vs the field

You now know enough to choose a framework *on purpose* rather than by reputation. These tools aren't
competing for the same spot - they're aimed at different sizes of problem and different tastes.

```mermaid
flowchart TD
  Start[Need a Node web service?] --> Learn{Want to see the raw machine?}
  Learn -- Yes --> Http[node:http by hand]
  Learn -- No, give me a framework --> Size{What matters most?}
  Size -- Simplicity + ubiquity --> Express[Express]
  Size -- Speed + built-in validation --> Fastify[Fastify]
  Size -- Structure for a big team --> Nest[NestJS]
```

A line on each:

- **Express** - minimal and everywhere. A thin layer over `node:http` giving you routing and the
  middleware chain, leaving the rest (body parsing, auth, validation, templating) to middleware you
  assemble yourself. Biggest ecosystem, most tutorials, the framework you're most likely to meet in a
  Node job. (You're here.)
- **Fastify** - built for speed and built around *schemas*. Declare a JSON schema for a route's body
  and reply, and Fastify uses it for both validation and fast serialization, with a plugin system
  instead of bare middleware. See [Fastify From Zero](/guides/fastify-from-zero).
- **NestJS** - opinionated and TypeScript-first. Brings structure: dependency injection, modules,
  controllers, decorators - an Angular-flavored architecture that pays off when an app and team get
  large. See [NestJS From Zero](/guides/nestjs-from-zero).
- **The bare foundation** - `node:http` itself, no framework. Knowing what Express saves you starts
  with knowing what you'd otherwise write by hand. See [Build a Server With node:http](/guides/build-a-server-with-node-http).

> 💡 Reach for **Express** for simplicity and ubiquity when you're happy assembling pieces yourself.
> Reach for **Fastify** for speed plus validation and serialization baked in. Reach for **NestJS** for
> enforced structure on a large app or team. None is "the best" - ask "best for *this* job?"

## A note on Express 5

📝 While you were learning, the goalposts moved in a good way. **Express 5 is now the current major
version**, mostly compatible with the Express 4 you've been writing - but it ships one quality-of-life
win worth calling out, because it touches Phase 6 directly.

In Express 4, an error thrown inside an `async` route handler would *not* reach your error-handling
middleware on its own - you had to catch it and pass it to `next(err)` yourself, or wrap every handler.
In **Express 5, async errors are forwarded automatically**: a rejected promise routes straight to your
error handler. The consistent error shape you built in Phase 6 now catches async failures with no extra
wrapping. When you start a new project, start it on Express 5.

## The ecosystem you'll reach for

Express stays small on purpose, so a real app is Express plus a handful of well-worn libraries. You
won't need all of these on day one, but you'll recognize them and know where each slots into the chain.

- **A real database, via an ORM.** Every API here stored tasks in memory - gone on restart. **Prisma**
  and **Drizzle** are the modern, TypeScript-first picks; **TypeORM** and **Sequelize** are still
  widely used; **Knex** if you want to stay closer to SQL. All do the same job - rows to objects and
  back. See [How an ORM Works](/guides/how-an-orm-works).
- **Auth.** **Passport** is the long-standing middleware for login strategies (sessions, OAuth); for
  token-based APIs, a JWT library lets each request prove who it is. It's the Phase 3 pattern - a
  function in the chain that checks the request and calls `next()` or rejects.
- **Validation.** You hand-rolled checks in Phase 4; real apps lean on **zod** (define a schema, parse
  the body, get typed data or a clean error), or **joi**/**express-validator**.
- **API docs.** `swagger-jsdoc` generates an OpenAPI spec from comments so others - and future you - 
  can read the contract.
- **TypeScript.** Add `@types/express` and your `req`, `res`, and `next` are typed. Most new Express
  code today is TypeScript, and everything you learned maps over directly.

## What to build

Reading more won't make this stick. Building one real thing will. Take the **tasks API** you grew
across this guide and carry it all the way home:

- **Swap the in-memory store for a real database** through an ORM (Prisma or Drizzle) so tasks survive
  a restart. If you kept data access separate as Phase 7 nudged, your routes barely change - you
  replace the bottom layer, not the top.
- **Add auth** - a JWT or session middleware so each request proves who it is, and tasks belong to a
  user. The Phase 3 middleware pattern doing a real job.
- **Validate with zod** instead of hand-written checks, returning the consistent error shape from
  Phase 6.
- **Add request logging** so you can see what your service is doing.
- **Deploy it** somewhere you can hit from your phone, wired up the way Phase 8 showed.

If the tasks API feels too familiar, build something small and new end to end instead - a **URL
shortener** or a **notes API**. Same muscles: routes, middleware, a store, validation, tests, deploy.

For a feel of the trade-offs, try this: **rebuild the same tasks API in Fastify or NestJS.** Nothing
teaches you what a framework gives and costs like porting an app you already understand.

An Express app is a chain of `(req, res, next)` functions running over `node:http` - and now that you
can see that chain, you can read *any* Node backend, not just the ones you wrote. Go give the tasks API
a database, lock it behind auth, deploy it, and show someone. You're ready.

## Recap

1. **You can ship a real Express API** - routed, parsed, validated, middleware-wrapped, structured,
   tested, and deployed - and you understand *why* each piece works, because Express hid nothing.
2. **Express is a pipeline of `(req, res, next)` functions** over `node:http`. Routes, parsers, auth,
   and the error handler are all that one shape in different costumes.
3. **Choose a framework on purpose** - Express for simplicity and ubiquity, Fastify for speed plus
   built-in validation, NestJS for structure on a large app or team, bare `node:http` for the raw
   machine.
4. **Express 5 is the current major** - mostly compatible with 4, with the big win that async errors
   are forwarded to your error handler automatically.
5. **The ecosystem fills the gaps** - an ORM (Prisma, Drizzle, TypeORM) for persistence, Passport or
   JWT for auth, zod for validation, swagger-jsdoc for docs, and TypeScript via `@types/express`.
6. **Build and finish one thing** - carry the tasks API to a database, auth, validation, logging, and a
   deploy; or port it to Fastify/Nest to feel the trade-offs.

## Quick check

Three decisions to take with you as you leave this guide:

```quiz
[
  {
    "q": "You want maximum throughput and you like declaring a schema once and getting both request validation and fast response serialization from it. Which framework fits best?",
    "choices": [
      "Express, because it's the most popular",
      "Fastify, which is schema-first and built for speed",
      "NestJS, because it uses TypeScript",
      "Bare node:http, always"
    ],
    "answer": 1,
    "explain": "Fastify is built around speed and schemas - one JSON schema drives validation and serialization. Express is minimal and ubiquitous; NestJS brings structure for large apps; node:http is the raw foundation."
  },
  {
    "q": "What is the notable improvement in Express 5 that touches your Phase 6 error handling?",
    "choices": [
      "It removes middleware entirely",
      "Async errors are forwarded to the error-handling middleware automatically, no manual next(err) wrapping needed",
      "It replaces node:http with fasthttp",
      "It makes res.json mandatory"
    ],
    "answer": 1,
    "explain": "In Express 4 you had to catch errors in async handlers and call next(err) yourself. Express 5 forwards rejected promises to your error handler automatically, so your one consistent error shape catches async failures with no extra wrapping."
  },
  {
    "q": "You're adding a real database to your tasks API and you kept data access separate as Phase 7 suggested. What mostly changes?",
    "choices": [
      "Every route handler must be rewritten from scratch",
      "Mainly the store layer swaps from an in-memory object to an ORM-backed one; the routes stay roughly the same",
      "You must abandon Express and switch to NestJS",
      "Nothing - Express persists data to a database automatically"
    ],
    "answer": 1,
    "explain": "Because the HTTP logic was kept separate from where data lives, your routes still parse, validate, call a store, and respond. You swap the store from an in-memory object to an ORM plus a database - the bottom layer changes, the top stays."
  }
]
```
