Getting Started
Express.js Fundamentals
Chapter 1 of 8 · Getting Started
Getting Started
Express is the most widely-used Node.js web framework — not because it does a lot, but precisely because it does very little. It adds routing, middleware, and a cleaner API around Node's built-in http module, then gets out of your way. Understanding what Express actually is (a thin wrapper, not a full framework) is the mental model that makes every other concept in this course click immediately.
Why Express Exists
Node's built-in http module can handle HTTP requests, but building even a simple API with it gets tedious fast. Every route is an if block, parsing request bodies requires manual buffering, and sending JSON means setting headers by hand.
| Task | Raw Node http | Express |
|---|---|---|
| Route GET /users | if (req.method === 'GET' && req.url === '/users') |
app.get('/users', handler) |
| Parse JSON body | Manually collect chunks, JSON.parse on end event |
app.use(express.json()) — then req.body |
| Send JSON response | res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify(data)) |
res.json(data) |
| URL parameters | Parse req.url with URL constructor manually |
app.get('/users/:id', ...) → req.params.id |
| Serve static files | Read file, determine MIME type, stream response | app.use(express.static('public')) |
Express doesn't replace Node's http module — it wraps it. A request that arrives at Node's http server is passed straight through to Express, which adds its routing layer and then calls your handler with enhanced req and res objects.
Installation & First Server
Run it with node server.js, or npx nodemon server.js during development (nodemon auto-restarts on file changes — essential for a smooth workflow).
The app Object
express() returns an application object — the central object in every Express app. It has methods for registering routes, configuring settings, and attaching middleware. Internally it is also a valid http.RequestListener, which is why you can pass it directly to http.createServer(app) — that's exactly what app.listen() does under the hood.
Route Methods
Express provides a method on the app object for every HTTP verb: app.get(), app.post(), app.put(), app.patch(), app.delete(), app.all(). Each takes a path pattern and one or more handler functions.
req and res — A First Look
Express wraps Node's raw IncomingMessage and ServerResponse objects, adding convenience properties and methods. Chapter 4 covers them in depth; here's the essential subset for Chapter 1.
The req Object
req.params — URL parameters (:id → req.params.id)
req.query — parsed query string (?sort=asc)
req.body — parsed request body (needs a body-parser middleware)
req.headers — request headers object
req.method — HTTP verb as a string ('GET')
req.path — path without query string ('/users/42')
The res Object
res.send(body) — send any response (string, Buffer, object)
res.json(data) — send JSON (sets Content-Type automatically)
res.status(code) — set status code; chainable
res.redirect(url) — send 302 redirect
res.set(field, value) — set a response header
res.sendFile(path) — stream a file to the client
The Request-Response Cycle
Recommended Project Structure
Express imposes no structure at all — you can put everything in one file. In Chapter 8 we'll look at the full MVC pattern, but this is a sensible starting shape for any project beyond a toy:
The key split: app.js creates and configures the Express app and exports it. server.js imports it and calls app.listen(). This lets test files import app without starting a real server, which is the pattern supertest expects.
⚡ Express Version: 4 vs 5
Express 5 (beta since 2015, stable release in 2024) is the current version and is what npm install express installs today. The most important change for your code: async route handlers that throw will automatically be caught and passed to error middleware — in Express 4 you had to wrap them in try/catch and call next(err) manually (or use the express-async-errors package as a patch). Chapter 6 covers error handling in detail, but be aware of which version you're on: express --version or npm list express.
All examples in this course work on both Express 4 and 5. Where there's a behavioural difference, it's noted.
📦 What Gets Installed
npm install express installs Express itself plus a small set of well-known dependencies (e.g. path-to-regexp for route matching, qs for query string parsing, depd for deprecation warnings). Express has deliberately kept its own dependency footprint minimal — everything else (body parsing beyond JSON, templating, sessions, authentication) is a separate npm install. That's a feature, not a limitation: you pay only for what you actually use.
Coding Challenges
Challenge 1 — Multi-Route Hello World
Build a self-contained Express server (single file, no external deps beyond express) with these four routes: GET / returns a JSON object { message: "Welcome to my API", version: "1.0" }; GET /health returns { status: "ok", uptime: <seconds> } using process.uptime(); GET /echo reads a ?message= query parameter and returns { echo: "<value>" }, responding 400 with { error: "message query param required" } if it's missing; GET /time returns the current UTC time as an ISO string. The server should listen on process.env.PORT || 3000 and log the port on startup.
Challenge 2 — app.js / server.js Split
Refactor the Challenge 1 server into the two-file structure: src/app.js creates and configures the Express app (mounts all four routes from Challenge 1) and exports it without calling listen(); server.js imports app, calls listen(), and attaches a SIGTERM handler that closes the server gracefully before exiting. Write a test file app.test.js using supertest that imports app directly (no running server) and verifies: GET / returns 200 with version: "1.0"; GET /health returns 200 with a numeric uptime; GET /echo without a query param returns 400; GET /echo?message=hello returns { echo: "hello" }.
Challenge 3 — Simple Request Logger
Write a custom middleware function requestLogger(req, res, next) that logs [METHOD] /path → STATUS_CODE (Xms) for every request (e.g. [GET] /health → 200 (3ms)). The tricky part: the status code isn't known until the response is sent, so you need to listen for the 'finish' event on res and calculate elapsed time using Date.now() or process.hrtime.bigint(). Mount it on the app from Challenge 2. Write a supertest test that makes two requests and captures console.log output (use jest.spyOn) to verify both log lines appear in the correct format.