Engineering Notes
Home/Backend/Frameworks/Express Fundamentals
Backend · Frameworks

Express Fundamentals

Express is a thin routing and middleware layer over Node's HTTP server. Follow requests through global middleware, a router and a handler, and see what happens when nothing matches or a handler throws.

⚙️ Backend● Beginner⏱ 5 min read▶ 1 interactive🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • What Express is (and isn't)
  • Routing by method and path; params vs query
  • res methods and body parsing
  • Global middleware with app.use() and the unopinionated design

1What Express Actually Is

Express is a thin routing and middleware layer on top of Node's built-in http module — it does not replace http.createServer, it wraps it. Every Express app, at its core, is still a single function passed to http.createServer(app) that gets called with (req, res) for every incoming request:

js
const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Hello, world!");
});

app.listen(3000, () => console.log("listening on :3000"));

Express's entire job is to look at req.method and req.url, decide which of your registered handlers should run, and give you a much nicer req/res API than the raw Node objects provide (raw http's res has no .json(), no .status() — Express adds all of that as a thin convenience layer on top of the same underlying ServerResponse object).

2Routing by Method and Path

js
app.get("/users", (req, res) => { /* list users */ });
app.post("/users", (req, res) => { /* create a user */ });
app.get("/users/:id", (req, res) => { /* get one user */ });
app.put("/users/:id", (req, res) => { /* replace a user */ });
app.delete("/users/:id", (req, res) => { /* delete a user */ });

Express registers each app.METHOD(path, handler) call into an internal list, checked in registration order against the incoming request's method and path. The first matching route wins — a route registered earlier "shadows" a later route with an overlapping pattern, which is a common source of "my handler never runs" bugs (see Common Pitfalls).

3Route Params vs. Query Params

js
// GET /users/42?include=orders
app.get("/users/:id", (req, res) => {
  console.log(req.params.id);       // "42"  — from the :id segment of the PATH
  console.log(req.query.include);   // "orders" — from the ?include=... QUERY STRING
  res.json({ id: req.params.id, include: req.query.include });
});

req.params comes from named segments in the route pattern itself (:id) — it only exists because the route was declared with that placeholder. req.query comes from parsing everything after ? in the URL, is available on every request regardless of route pattern, and every value is always a string (or array of strings for repeated keys) — Express/Node never infers numbers or booleans from the query string.

4res Methods and Response Shaping

js
res.status(201).json({ id: "u1", name: "Ana" }); // sets status THEN sends JSON body
res.send("plain text or HTML");                   // content-type inferred from what you pass
res.sendStatus(404);                               // shortcut: sets status AND sends its text as body
res.redirect("/login");                             // 302 by default
res.set("X-Request-Id", requestId);                 // set a response header

res.status(code) returns res itself (so it chains into .json()/.send()), and does not send the response on its own — nothing goes out over the socket until you call .send(), .json(), .end(), or similar. Forgetting to call one of those terminal methods leaves the client hanging with no response until it times out.

5Parsing Request Bodies

js
app.use(express.json());              // parses application/json bodies into req.body
app.use(express.urlencoded({ extended: true })); // parses HTML form submissions

app.post("/users", (req, res) => {
  console.log(req.body); // { name: "Ana", email: "ana@example.com" } — only populated
                          // because express.json() ran first and parsed the raw stream
});

Without registering express.json() first, req.body is undefined even for a request that clearly has a JSON body — Express does not parse request bodies by default, because reading and buffering the body is itself a design choice (streaming large uploads shouldn't be forced through a JSON parser). This is the first concrete example of middleware ordering mattering, expanded on next.

6Global Middleware with app.use()

js
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`); // runs for EVERY request, before any route handler
  next(); // MUST call this, or the request hangs forever — nothing downstream ever runs
});

app.get("/", (req, res) => res.send("home"));

app.use() without a path registers a handler that runs for every request, in the order it was registered, before the matching route handler — this is Express's middleware chain, covered in depth in the dedicated 06-middleware/ topic in this group. The critical rule introduced here: every middleware function must call next() (to hand off to the next one in the chain) or send a response itself — doing neither leaves the request permanently unresolved.

Express keeps an ordered stack of layers, body parser, cors, routes, 404 and error handlers, tried top to bottom; handlers read params, query and body and answer through res methods
Express keeps an ordered stack of layers, body parser, cors, routes, 404 and error handlers, tried top to bottom; handlers read params, query and body and answer through res methods

7Minimal and Unopinionated by Design

Express deliberately does not prescribe a folder structure, a dependency-injection system, a validation library, or an ORM — it hands you app.get/post/..., app.use(), and a nicer req/res, and nothing else. This is the direct contrast with NestJS (covered in 02-nestjs_fundamentals/), which is built on top of Express (or Fastify) and adds an opinionated, decorator-based structure — modules, controllers, dependency injection — as a deliberate answer to the fact that large Express codebases tend to reinvent that structure inconsistently, project by project, once they outgrow a handful of route files.

⚠️ Common Pitfalls

  1. Registering a more general route before a more specific one that should take precedence. app.get("/users/:id", ...) registered before app.get("/users/me", ...) means /users/me gets swallowed by the :id pattern ("me" matches :id just fine) and the more specific handler never runs — order matters, most-specific-first.
  2. Forgetting express.json() before reading req.body. req.body is silently undefined, not an error — easy to miss until a handler mysteriously "loses" data that was clearly sent.
  3. Calling res.json(...) more than once for the same request. Node throws ERR_HTTP_HEADERS_SENT — a common cause is an if branch that sends a response but forgets to return, so code after it also tries to send.
  4. Forgetting to call next() in a middleware that doesn't itself send a response. The request hangs until the client times out — no error is thrown, which makes it easy to miss in testing.
  5. Doing real business logic directly inside route handlers. Works fine for a tiny app, but doesn't scale — this is exactly the thin-controller principle covered in 05-controllers/.

📋 Quick Reference

text
app.get/post/put/delete(path, handler)  -- register a route
req.params.<name>   -- from :name in the route PATH (only exists if declared)
req.query.<name>     -- from ?name=... in the URL, always present, always a string
res.status(code).json(obj)  -- chainable; nothing sent until a terminal method is called
app.use(fn)           -- global middleware, runs in registration order, before route handlers
express.json()         -- MUST be app.use()'d before req.body is populated for JSON requests
next()                  -- hands off to the next middleware/handler; omitting it hangs the request

🧠 Key takeaways

  • Express = routing + middleware on top of http.createServer; it imposes no structure.
  • Routes match method + path; req.params from the path, req.query from the query string.
  • Middleware runs in registration order; each calls next() or ends the response.
  • Add express.json() for bodies, a catch-all 404, and a 4-argument error middleware last.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How does Express handle a request?core▾

It runs a chain of middleware functions (req, res, next) in registration order; each can modify req/res, end the response, or call next() to continue. Routers are middleware too. Errors go to error-handling middleware with four arguments (err, req, res, next).

Follow-ups interviewers ask
  • What happens if no middleware responds? The request hangs until timeout (or hits a 404 handler if defined).
Q2How do you handle async errors in Express?core▾

Express 4 doesn't catch rejected promises from async handlers; you must wrap them (express-async-errors, a wrapper, or try/catch + next(err)). Express 5 forwards rejected promises to error middleware automatically.

Follow-ups interviewers ask
  • Consequence of forgetting? Unhandled rejections and hanging requests.
Q3How would you structure a production Express app?core▾

Separate routes, controllers, services and data access; centralized error handling; validation middleware (Zod/Joi); security middleware (helmet, CORS config, rate limiting); structured logging (pino); config via environment; graceful shutdown.

Follow-ups interviewers ask
  • What's helmet? Middleware setting security headers.
Q4How do you implement graceful shutdown?senior▾

On SIGTERM: stop accepting new connections (server.close()), let in-flight requests finish with a timeout, close DB pools and queues, then exit. Fail readiness probes first so load balancers stop sending traffic.

Follow-ups interviewers ask
  • Why does it matter in Kubernetes? Pods get SIGTERM on every deploy; abrupt exits drop requests.
Q5Express vs Fastify vs NestJS?core▾

Express: minimal, huge ecosystem, unopinionated. Fastify: faster, schema-based validation and serialization, plugin encapsulation. NestJS: opinionated framework with modules, DI and decorators, built on Express or Fastify, suited for large teams.

Follow-ups interviewers ask
  • Why is Fastify faster? Optimized routing and JSON serialization compiled from schemas.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. In /users/:id?expand=orders, where is 'orders'?
Query string.
2. Why is req.body undefined?
Order matters.
3. How does Express recognize error middleware?
Arity.
4. Express is…
You choose the structure.