🚂 Introduction to Express
Last week you learned to build a raw HTTP server with Node's http module — and quickly discovered how much bookkeeping it demands. Express is the framework that sweeps all that boilerplate away, giving you clean routing, a middleware pipeline, and helper methods so you can focus on what your server does instead of how it wires bytes together.
Week 7 · Day 2 (Tuesday: Express.js Basics) · Lecture 1
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what Express is and the specific problems it solves over the raw
httpmodule - Install Express into a Node project and read what
npmrecords inpackage.json - Write, run, and stop a minimal Express server that responds on multiple routes
- Trace a request through the Express pipeline: middleware → route handler → response
- Use the built-in
express.json()body parser andexpress.static()without extra packages - Recognize a sensible starting project structure for an Express app
Estimated Time: 60 minutes
Practice: Scaffold a fresh Express project and build a tiny greeting API from scratch.
In This Lesson
What Is Express?
Express is a small, unopinionated web framework for Node.js. "Unopinionated" means it gives you a handful of powerful tools — routing, a middleware system, and response helpers — but doesn't force you into a particular folder layout, database, or template engine. You assemble the pieces you need and leave the rest.
It has been the default choice for Node web servers for over a decade. Countless tutorials, tools, and hosting guides assume it, and its middleware ecosystem is enormous. Learning Express is the fastest on-ramp to writing real backends and APIs in JavaScript.
🧱 The LEGO analogy
Think of Node as the plastic baseplate — the raw runtime. Express is a box of pre-shaped LEGO bricks that snap together cleanly: a routing brick, a middleware brick, a JSON brick. You still build the model yourself, and you can always reach past the bricks to the bare baseplate (raw Node) when you need to — but for most work, the bricks get you there far faster with far less glue.
What Express actually gives you
- Routing — match a URL path and HTTP method to a function, without hand-writing
ifchains. - Middleware — a pipeline of functions that each request flows through, for logging, parsing, auth, and more.
- Response helpers —
res.json(),res.status(),res.redirect(), and friends, so you rarely touch raw headers. - Static file serving and view rendering built in.
Express vs. Raw Node
The quickest way to feel Express's value is to build the same tiny server twice. Here is a server that answers a few routes using only the http module you met last lesson.
Raw Node.js
// server-vanilla.js — the http module, no framework
const http = require('http');
const server = http.createServer((req, res) => {
// We must parse and branch on the URL ourselves:
if (req.url === '/' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Hello, World!</h1>');
} else if (req.url === '/api/users' && req.method === 'GET') {
const users = [{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }];
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users)); // manual stringify + manual header
} else {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>404 Not Found</h1>');
}
});
server.listen(3000, () => console.log('Server on http://localhost:3000'));
Express
// server-express.js — the same behaviour, far less plumbing
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('<h1>Hello, World!</h1>'); // sets 200 + Content-Type for you
});
app.get('/api/users', (req, res) => {
const users = [{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }];
res.json(users); // stringifies + sets JSON header
});
// Anything unmatched falls through to this catch-all 404:
app.use((req, res) => {
res.status(404).send('<h1>404 Not Found</h1>');
});
app.listen(3000, () => console.log('Server on http://localhost:3000'));
Both listen on port 3000 and return the same bytes. But notice what disappeared in the Express version: no manual URL string comparisons, no writeHead with hand-typed content types, no JSON.stringify. Every route reads like a plain sentence — "on a GET to /api/users, send this JSON."
| Concern | Raw Node http | Express |
|---|---|---|
| Routing | Manual if/else on req.url | app.get(), app.post(), … |
| Content type | Set by hand in writeHead | Inferred by res.send() / res.json() |
| Status codes | First arg of writeHead | Chainable res.status(201).json(...) |
| Request body | Collect data chunks yourself | express.json() fills req.body |
| Reusable logic | Hand-rolled wrappers | Middleware pipeline |
💡 When is raw Node still fine?
For a one-file health check, a build script, or squeezing out the absolute last drop of performance, the bare http module is perfectly good. The moment you have more than a couple of routes, need to parse bodies, or want to share logic across endpoints, Express earns its keep.
Setting Up a Project
Express is a single npm package. Setup is three commands.
Step 1 — Create and initialise the project
mkdir express-demo
cd express-demo
npm init -y
npm init -y writes a package.json with sensible defaults (the -y means "yes to every prompt"). That file is the manifest of your project — its name, scripts, and dependency list.
Step 2 — Install Express
npm install express
This downloads Express into node_modules/ and records it under "dependencies" in package.json:
// package.json (excerpt)
{
"name": "express-demo",
"version": "1.0.0",
"type": "commonjs",
"scripts": {
"start": "node app.js",
"dev": "node --watch app.js"
},
"dependencies": {
"express": "^4.21.0"
}
}
✅ Modern Express — no body-parser needed
Older tutorials tell you to npm install body-parser. Since Express 4.16 (2016), JSON and URL-encoded body parsing are built in as express.json() and express.urlencoded(). Skip the extra package.
Step 3 — A dev script that restarts on save
Node 18+ ships a built-in --watch flag that reloads your server whenever a file changes, so you no longer need nodemon for basic work:
npm run dev # runs: node --watch app.js
Your First Server
Create app.js. This version shows the pieces you'll use every day: the built-in JSON parser, static file serving, a couple of routes, and a route with a URL parameter.
// app.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// --- Middleware (runs on every request, top to bottom) ---
app.use(express.json()); // parse JSON bodies into req.body
app.use(express.static('public')); // serve files from ./public
// --- Routes ---
app.get('/', (req, res) => {
res.send('<h1>Welcome to Express!</h1>');
});
// A dynamic segment: :id becomes req.params.id
app.get('/users/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
app.get('/api/info', (req, res) => {
res.json({ name: 'Express Demo API', version: '1.0.0', status: 'active' });
});
// Reads the JSON body that express.json() parsed for us
app.post('/api/echo', (req, res) => {
res.status(201).json({ youSent: req.body });
});
// --- Start listening ---
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Run it and visit the routes:
node app.js
# then open http://localhost:3000/ and http://localhost:3000/api/info
# stop the server with Ctrl+C
Test the POST route from another terminal with curl:
curl -X POST http://localhost:3000/api/echo \
-H "Content-Type: application/json" \
-d '{"hello":"world"}'
Output
{"youSent":{"hello":"world"}}
⚠️ Without express.json(), req.body is undefined
If you POST JSON but forget the app.use(express.json()) line, req.body won't exist and you'll get undefined (or a crash reading a property of it). The parser middleware is what fills req.body — and it must be registered before the routes that need it.
The Request Pipeline
The single most important mental model in Express is the pipeline: every incoming request flows through your registered middleware functions in order, then into a matching route handler, which sends a response. This is the diagram to burn into memory.
Each middleware function receives (req, res, next). It can inspect or modify the request, and then either call next() to pass control down the pipeline, or end the cycle by sending a response. A route handler is just the final middleware that sends the response.
// A middleware is a function of (req, res, next)
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`); // do some work...
next(); // ...then hand off to the next stage
});
We'll spend the whole next lesson on middleware. For now, the key takeaway is order matters: middleware you register earlier runs earlier. That's why the JSON parser goes near the top and the 404 catch-all goes at the very bottom.
Project Structure
A single app.js is perfect while you're learning. As an app grows, you split it into folders so each part has one job. You'll build up to this over the coming lessons — here's the destination:
express-demo/
├── node_modules/
├── public/ # static assets served as-is (css, images)
├── src/
│ ├── routes/ # route definitions (express.Router)
│ ├── controllers/ # the functions that handle each route
│ ├── middleware/ # custom middleware (auth, logging, errors)
│ └── models/ # data access
├── app.js # wires everything together
└── package.json
The guiding idea is separation of concerns: routes say which URL, controllers say what to do, middleware handles cross-cutting work like auth. Don't build all of this today — just know it's where you're headed, so today's single file doesn't feel like the final shape.
💡 Environment variables from day one
Notice const PORT = process.env.PORT || 3000; in our server. Reading configuration from the environment (rather than hard-coding it) is a habit worth forming immediately — hosting platforms set PORT for you, and secrets should never be typed into source.
Practice & Quiz
🏋️ Exercise 1: A greeting API from scratch
Goal: Build a fresh Express app with two routes: GET /hello/:name returns a greeting, and POST /greet reads a JSON body { "name": "..." } and returns a greeting from it.
# Setup
mkdir greet-api && cd greet-api
npm init -y
npm install express
💡 Hint
Remember to register app.use(express.json()) before your POST route, or req.body will be undefined. The URL parameter :name arrives as req.params.name.
✅ Solution
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/hello/:name', (req, res) => {
res.json({ message: `Hello, ${req.params.name}!` });
});
app.post('/greet', (req, res) => {
const { name } = req.body;
if (!name) {
return res.status(400).json({ error: 'name is required' });
}
res.status(201).json({ message: `Hello, ${name}!` });
});
app.listen(3000, () => console.log('Listening on http://localhost:3000'));
🏋️ Exercise 2: Add a catch-all 404
Goal: Extend Exercise 1 so any unmatched route returns a JSON 404 instead of Express's default HTML page.
✅ Solution
// Place this AFTER all your other routes:
app.use((req, res) => {
res.status(404).json({ error: `No route for ${req.method} ${req.originalUrl}` });
});
Because middleware runs in order, this only fires when no earlier route matched.
🎯 Quick Quiz
Question 1: Which package must you install to parse JSON request bodies in a modern Express app?
Question 2: Why must app.use(express.json()) come before your route handlers?
Question 3: What does res.json({ ok: true }) do that res.send of a raw string does not?
Best Practices & Pitfalls
✅ Do
- Register body parsers (
express.json()) and security middleware near the top of the file - Read the port from
process.env.PORTwith a local fallback - Use
res.json()for data andres.status(code)to be explicit about outcomes - Put a catch-all 404 handler last, after every real route
- Add a
startanddevscript topackage.jsonso running the app is one command
❌ Don't
- Install
body-parser— the built-in parsers replaced it years ago - Send a response and call
next()in the same handler — pick one - Hard-code secrets or ports directly in source files
- Forget to stop an old server (
Ctrl+C) before starting a new one on the same port
⚠️ "EADDRINUSE: address already in use"
Error: listen EADDRINUSE: address already in use :::3000
This means a previous server is still holding port 3000. Stop it with Ctrl+C in its terminal, or start your new server on a different port. It's the single most common beginner surprise.
Summary
🎉 Key Takeaways
- Express is a thin framework over Node's
httpmodule that removes routing and response boilerplate - Setup is three commands:
npm init -y,npm install express, writeapp.js - Routes read like sentences:
app.get('/path', handler) - Every request flows through a middleware pipeline in registration order, then a route handler, then a response
express.json()andexpress.static()are built in — nobody-parserneeded
📚 Additional Resources
- Express — Installing
- Express — Hello World example
- Express — Basic routing
- MDN — Express/Node introduction
🚀 What's Next?
You've stood up a server and seen requests flow through it. Next we go deep on the two mechanisms that make Express powerful: defining rich routes and building the middleware pipeline that every request passes through — Routing and Middleware.
🎉 Your first server is live!
From here on, every API and web backend you build in this bootcamp starts exactly the way this one did.