In Module 4 you wrote a complete HTTP server without a single dependency: a router with patterns compiled into regular expressions, a body reader with a size limit, a static file server with ETag and compression, and a table that translated domain error codes into HTTP statuses. It was a lot of work, and it was work well spent: now you know exactly what a web framework does under the hood. In this module Express arrives, and your experience is going to be different from that of the beginner who learns it from scratch. Where others see magic, you are going to recognize pieces you already built.

This first lesson answers a very specific question: what problem does Express actually solve. Not "why is it popular" or "how is it used in general", but which lines of code disappear from your project when you install it, and which lines stay yours forever.

Contents

  1. What a web framework is
  2. The honest comparison: the same endpoint, twice
  3. What Express gives you and what stays yours
  4. The minimalist philosophy and its consequences
  5. The ecosystem: real alternatives
  6. Installing Express with Module 5 eyes
  7. Express 5 versus Express 4
  8. The first app.js for Escena Viva
  9. Taking the magic apart: req and res are the ones from node:http
  10. What app really is

  1. What a web framework is

A web framework is a library that sits between the node:http module and your business code, and that solves the problems that show up always, in any HTTP server, whatever the domain. Nobody writes an API without needing to:

  • Decide which function handles each combination of method and path (routing).
  • Extract parameters from the URL (/events/evt-001 → id = 'evt-001').
  • Run common tasks before every handler: log the request, read the body, check headers (middleware).
  • Serialize JSON responses with the correct Content-Type and length.
  • Serve static files.
  • Catch errors at a single point and turn them into consistent responses.

You solved those six problems by hand in Module 4. A framework solves them for you, and it does so with code that thousands of people have reviewed over more than a decade.

The key word is cross-cutting: the framework knows nothing about capacity, sessions or tickets. That part is yours. The framework knows about HTTP.

  1. The honest comparison: the same endpoint, twice

Let's put the same endpoint, GET /events/:id, side by side: as it ended up in Module 4 and as it looks with Express. No tricks: the hand-written code is the code you wrote, and it does exactly the same thing.

Hand-written version (Module 4)

// Extract from src/server/api-routes.js in module 4 (abridged).
const { createRouter } = require('./router.js');
const { sendJson, sendError } = require('./responses.js');
const { getEventById } = require('../catalog-data.js');

const router = createRouter();

// 1. Registering the route: the pattern is compiled into a regular expression.
router.register('GET', '/events/:id', async (req, res, params) => {
  try {
    // 2. The parameter arrives in an object built by my own matcher.
    const event = await getEventById(params.id);
    // 3. Serialization, Content-Type and Content-Length are mine.
    sendJson(res, 200, event.toJSON());
  } catch (error) {
    // 4. Translating the domain code into an HTTP status is mine, and on top of
    //    that I have to remember to wrap EVERY async handler in try/catch.
    sendError(res, error);
  }
});

module.exports = { router };

And that does not count the pieces it depends on: router.js (compiling patterns, matching, handling the 405 with Allow), responses.js (four writing functions) and http-errors.js (the status table). Between the three of them there are several hundred lines that you had to write, understand and maintain.

Express version

// src/routes/events.js with Express 5.
const express = require('express');
const { getEventById } = require('../catalog-data.js');

const eventRoutes = express.Router();

eventRoutes.get('/:id', async (req, res) => {
  const event = await getEventById(req.params.id);
  res.json(event.toJSON());
});

module.exports = { eventRoutes };

Four lines of logic against roughly fifteen, and above all zero lines of infrastructure. Look at exactly what has disappeared:

Module 4 piece What happens with Express
compilePattern / match from router.js router.get('/:id') does it internally
Building the params object It arrives as req.params
sendJson (headers + JSON.stringify) It is res.json()
The try/catch in every async handler In Express 5 the rejection goes to the error handler on its own
The 405 with the Allow header Express handles it while walking the routes
normalizePath (trailing slashes, casing) The strict routing and case sensitive routing settings

What matters is not that there are fewer lines. It is that the lines left talk only about Escena Viva: look up an event and return it. Everything else was necessary noise that somebody now maintains for you.

  1. What Express gives you and what stays yours

This is where many courses lie by omission. Express solves a bounded set of problems, and there is an equally long list of things that are still your responsibility.

Area Does Express solve it? Comment
Routing and parameters Yes The core of the framework
Middleware chain Yes The core of the framework
Responses (json, send, status, sendFile) Yes Replaces your responses.js
Static files with ETag and caching Yes (express.static) Replaces static.js + mime-types.js
Reading the body with a limit Yes (express.json({ limit })) Replaces body.js, 413 included
A single point for error handling Yes (four-argument middleware) But you define the error policy
Input data validation No Lesson 06-06, with zod
Security (headers, CORS, limits) No Third-party middleware, lesson 06-05
Authentication and authorization No All of Module 8
Data access / database No Module 7
Folder and layer architecture No Lesson 06-02: you decide
Structured logging and observability No Module 11
Testing No Module 9, with supertest

Read it twice. Express saves you the HTTP plumbing. It does not save you from thinking. A badly designed Express server is exactly as bad as a badly designed node:http server, only shorter.

  1. The minimalist philosophy and its consequences

Express describes itself as fast, unopinionated, minimalist. The word that matters most is unopinionated. Express does not tell you where to put your folders, how to separate controllers from services, which validator to use or how to structure your errors. It only gives you the mechanism.

That cuts both ways:

Advantages

  • You can adapt the structure to your real project, not to the documentation's example.
  • The API surface you have to learn is small: routing, middleware, req, res, and little else.
  • It integrates with anything, because it imposes nothing.

Drawbacks

  • Two Express projects may look nothing alike, which makes stepping into somebody else's code harder.
  • You have to make decisions that an opinionated framework would make for you: validation, errors, layers, configuration.
  • It is easy to end up with an 800-line app.js if nobody enforces discipline.

This module is precisely about that: making those decisions well and once, and writing them into the structure of the Escena Viva project.

  1. The ecosystem: real alternatives

Express is not the only option, and it pays to know what sits next to it so you don't defend a tool out of habit.

Framework Core idea Strong at In exchange
Express Minimalist middleware Huge ecosystem, documentation and answers everywhere, gentle learning curve Barely opinionated; validation and schemas are on you
Fastify Performance and JSON schemas Very fast, built-in schema validation and serialization, encapsulated plugins Smaller ecosystem, concepts of its own (encapsulation, hooks)
Koa Middleware with async/await and a ctx context Tiny, elegant core, fine-grained flow control Almost everything is a separate package; less teaching material
NestJS Opinionated architecture (decorators, dependency injection) Large projects and large teams, structure imposed for you A lot to learn before your first route; TypeScript in practice
Hapi Declarative configuration Built-in validation and security, everything through configuration A very distinctive style, smaller community

Why this course teaches Express: because the concepts you learn here (chained middleware, routing, centralized error handling) are the same ones Koa, Fastify and NestJS use with different syntax, and because it is the framework you will run into most often in existing code. Learning Express is learning the vocabulary of the ecosystem.

  1. Installing Express with Module 5 eyes

Let's install it, but not with our eyes closed. In Module 5 you learned to audit what lands in node_modules; apply that now.

# Install as a production dependency.
npm install express

# Which version came in exactly?
npm ls express

# How many packages are in the tree in total now?
npm ls --all --parseable | wc -l

# Package license and metadata.
npm view express version license repository.url

# Security audit after installing.
npm audit

What you should observe and how to read it:

  • Express is not a single piece: it drags in several dozen transitive dependencies (router, body-parser, send, serve-static, cookie, mime-types, finalhandler, qs…). Don't panic: most of them are tiny modules from the same team, and many are literally the pieces you wrote by hand (send is your static.js, mime-types is your mime-types.js, body-parser is your body.js).
  • MIT license, permissive, with no obligations for your project.
  • Express is a project of the OpenJS Foundation, with public governance. That is exactly the kind of signal you learned to look for: it does not depend on a single person.
  • Pin the version in package.json and respect package-lock.json: in deployment you install with npm ci, not with npm install.

After installing, your package.json will include something like:

{
  "dependencies": {
    "dotenv": "^17.0.0",
    "express": "^5.1.0"
  }
}

  1. Express 5 versus Express 4

Express 5 is the current stable version, and there is a lot of code and a lot of tutorials written for version 4. These are the differences that will really affect you:

Aspect Express 4 Express 5
async handlers that reject They hung unless you wrapped the handler The rejection goes automatically to the error handler
req.query A mutable object, you could reassign it Read-only getter: req.query = ... throws
Wildcards /files/* with req.params[0] /files/*path with req.params.path (a name is mandatory)
Optional parameters /events/:id? /events{/:id} (optional group syntax)
body-parser A separate package for some cases express.json() and express.urlencoded() built in
res.status(...) with an invalid code It was accepted Throws if it is not an integer from 100 to 999

The first two rows are the ones that break the most old code. The first is a huge improvement that we will cover in depth in 06-07; the second causes a very specific error when somebody tries to "clean up" the query by reassigning it, and we will deal with it in 06-06.

  1. The first app.js for Escena Viva

Ten useful lines. Create it as app-minimal.js in the project root just for this test; in 06-02 we will structure it properly.

// app-minimal.js
const express = require('express');

const app = express();

// A health route, just like the GET /health from module 4.
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// An endpoint with a route parameter.
app.get('/events/:id', (req, res) => {
  res.json({ id: req.params.id, venue: 'Teatro Almendra' });
});

app.listen(3000, () => {
  // Diagnostics on stderr, as the course convention dictates.
  console.error('Escena Viva listening on http://localhost:3000');
});
node app-minimal.js
curl -s http://localhost:3000/health
curl -s http://localhost:3000/events/evt-001
curl -si http://localhost:3000/route-that-does-not-exist | head -n 1

Go over the result carefully:

  • GET /health returns JSON with Content-Type: application/json; charset=utf-8 without you setting the header. That is res.json().
  • GET /events/evt-001 returns the extracted parameter. That is what your compilePattern did.
  • A route that does not exist returns 404 Not Found with a default HTML body. In 06-07 we will replace it with our own { error: { code, message, status } } format.

  1. Taking the magic apart: req and res are the ones from node:http

This is the most important idea in the lesson. The req and res objects in Express are not new objects: they are exactly the IncomingMessage and the ServerResponse from node:http you already know from Module 4, with methods added to their prototypes. Let's prove it.

// app-demo.js
const http = require('node:http');
const express = require('express');

const app = express();

app.get('/test', (req, res) => {
  // 1. They are instances of the Node core classes.
  const isCoreRequest = req instanceof http.IncomingMessage;
  const isCoreResponse = res instanceof http.ServerResponse;

  // 2. The node:http methods are still there and still work.
  res.setHeader('X-Escena-Viva', 'Teatro Almendra');
  res.statusCode = 200;

  // 3. And the request is still a readable stream from module 3.
  const isStream = typeof req.on === 'function' && typeof req.pipe === 'function';

  // 4. An Express method, which internally uses the three above.
  res.json({ isCoreRequest, isCoreResponse, isStream });
});

app.listen(3000);
curl -si http://localhost:3000/test

Response:

HTTP/1.1 200 OK
X-Escena-Viva: Teatro Almendra
Content-Type: application/json; charset=utf-8

{"isCoreRequest":true,"isCoreResponse":true,"isStream":true}

The practical consequences of this will serve you throughout the whole module:

  • Everything you learned in Module 4 about res.writeHead, res.end, req.headers or req.method is still valid inside Express.
  • res.json(object) is roughly: set Content-Type, serialize with JSON.stringify and call end(). It is your sendJson, with more edge cases covered (ETag, json spaces, special characters).
  • Since req is still a stream, you can read it with pipeline for large uploads instead of buffering it in memory.
  • If something in Express does not do what you want, you can always drop one floor down to the node:http API. There is no black box; there is a thin layer.

  1. What app really is

A second demystification. express() returns a function. Specifically, a function with the signature (req, res, next) that also has properties and methods hung off it (get, post, use, set, listen…). And that signature will look familiar: it is the signature of an http.createServer handler.

const http = require('node:http');
const express = require('express');

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

// The proof: app is a function.
console.error('Type of app:', typeof app); // 'function'

// And that is why it can be passed straight to createServer.
const server = http.createServer(app);
server.listen(3000, () => console.error('Our own server with an Express app'));

app.listen(3000) is just a shortcut that internally does http.createServer(this).listen(3000). Knowing this is not trivia: in 06-02 we will create the server by hand precisely so we can shut it down gracefully, and in Module 12 we will need it to attach Socket.IO to the same server.

Put another way: Express is a very sophisticated handler function. All the framework's magic amounts to this: when that function runs, it walks a list of registered middleware and routes and calls the ones that match. Nothing more. In 06-04 we will see it with a diagram.

Common Mistakes and Tips

  • Believing that Express "is" the backend. Express is an HTTP transport layer. Your business logic (the Event, Session and SalesManager classes) must not know that Express exists. If one day you switch to Fastify, the domain is untouched.
  • Following Express 4 tutorials with Express 5 installed. The typical symptoms: routes with :id? that stop working, an unnamed * that throws at startup, and code that reassigns req.query and fails. Always check npm ls express.
  • Installing without looking. Apply the Module 5 criteria to every package, including the ones in 06-05. Express is trustworthy; not every third-party middleware is.
  • Wrapping everything in try/catch out of habit. In Express 5 you don't need it for the general case; only when you want to turn the error into a more specific one before propagating it.
  • Forgetting that app is a function. When something odd happens at startup, remember that you can build the server yourself and debug at the node:http level.
  • Tip: keep the Module 4 code in the repository. Throughout this module we are going to compare it with the new code, and that comparison is what turns Express into something you understand rather than something you copy.

Exercises

Exercise 1: audit the installation

Install Express in the Escena Viva project and answer in writing: which exact version came in, which license it has, how many packages are in the dependency tree before and after, and which of those transitive dependencies match files you wrote by hand in Module 4.

Exercise 2: the health route, twice

Write two files that expose the same GET /health endpoint returning { status: 'ok', venue: 'Sala Boveda' }: one with plain node:http (without using your Module 4 router, everything in the handler) and another with Express. Count the lines in each one and note down what you had to do by hand in the first.

Exercise 3: prove the family ties

Create a GET /diagnostics endpoint in Express that returns JSON with: the class name of req (req.constructor.name), the class name of res, the list of headers the response has already set before sending, and the value of req.httpVersion. Also add a custom header using setHeader to confirm that the node:http API is still available.

Solutions

Solution 1

npm install express
npm ls express                       # e.g. [email protected]
npm view express license             # MIT
npm ls --all --parseable | wc -l     # compare before and after

Correspondences with your Module 4 code:

Transitive dependency Your Module 4 file
body-parser src/server/body.js
serve-static and send src/server/static.js
mime-types src/server/mime-types.js
router / path-to-regexp src/server/router.js
finalhandler The final 404/500 in handleRequest

Solution 2

// health-http.js — with plain node:http.
const http = require('node:http');

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  if (req.method !== 'GET') {
    res.writeHead(405, { Allow: 'GET', 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: { code: 'METHOD_NOT_ALLOWED', status: 405 } }));
    return;
  }
  if (url.pathname !== '/health') {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: { code: 'ROUTE_NOT_FOUND', status: 404 } }));
    return;
  }
  const body = JSON.stringify({ status: 'ok', venue: 'Sala Boveda' });
  res.writeHead(200, {
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': Buffer.byteLength(body),
  });
  res.end(body);
});

server.listen(3000);
// health-express.js — with Express.
const express = require('express');

const app = express();
app.get('/health', (req, res) => {
  res.json({ status: 'ok', venue: 'Sala Boveda' });
});
app.listen(3000);

By hand you had to: parse the URL, check the method, produce the 405 with Allow, produce the 404, serialize, compute Content-Length and write the headers. Express does all seven.

Solution 3

// diagnostics.js
const express = require('express');

const app = express();

app.get('/diagnostics', (req, res) => {
  res.setHeader('X-Escena-Viva-Venue', 'Auditorio Ribera');
  res.json({
    requestClass: req.constructor.name,       // IncomingMessage
    responseClass: res.constructor.name,      // ServerResponse
    headersAlreadySet: res.getHeaderNames(),  // includes x-escena-viva-venue
    httpVersion: req.httpVersion,             // '1.1'
  });
});

app.listen(3000, () => console.error('Diagnostics on :3000'));

The relevant part of the response: the class names are the Node core ones, not Express-specific classes, and getHeaderNames() is a ServerResponse method that is still available.

Conclusion

Express is not magic: it is the abstraction of the six cross-cutting tasks of every HTTP server —routing, parameters, middleware, responses, static files and errors— solved by hand by you in Module 4 and now delegated to mature, audited code. You have seen the line-by-line comparison, the honest list of what Express does not do for you (validation, security, architecture, data, testing), the alternatives in the ecosystem, and the two ideas that dismantle the apparent magic: req and res are the node:http objects with methods added, and app is simply a handler function that can be passed to http.createServer.

You also know now how Express 5 differs from Express 4 and why those differences will save you code in 06-07.

What you have right now is a toy app.js: ten lines in the project root. That is not a maintainable application. In the next lesson, Setting Up an Express Application, we will make the jump: we will separate createApplication() from starting the server —a separation that turns out to be mandatory in order to test with supertest in Module 9 and for the graceful shutdown in Module 11—, we will define the Escena Viva folder structure, configure the application settings (trust proxy, x-powered-by, strict routing) and centralize per-environment configuration in a module that fails fast if something is missing.

Node.js Course: From Beginner to Advanced

Module 1: Introduction to Node.js

Module 2: Core Concepts

Module 3: File System and I/O

Module 4: HTTP and Web Servers

Module 5: NPM and Package Management

Module 6: The Express.js Framework

Module 7: Databases and ORMs

Module 8: Authentication and Authorization

Module 9: Testing and Debugging

Module 10: Advanced Topics

Module 11: Deployment and DevOps

Module 12: Real-World Projects

© Copyright 2026. All rights reserved