Express & Routing
Learn how Express turns raw Node.js into a pleasant web server — with clean routes, URL parameters, and helpful request/response tools.
In the last lesson you built a Node.js HTTP server from scratch. It worked — but look at that code again: you had to check req.url with a long chain of if statements just to handle a handful of different paths. Add ten more routes and things get messy fast. Express is the most popular Node.js framework precisely because it solves this problem. Instead of one giant request handler, Express gives you a clean, readable API where each route is its own self-contained line. The result looks less like plumbing and more like a table of contents for your server.
#Why Use a Framework at All?
A framework is just someone else's code that handles the tedious, repetitive parts so you can focus on what makes your app unique. Express wraps Node's built-in http module and adds:
- Routing — map any HTTP method and URL pattern to a function
- Middleware — small, chainable functions that process requests (more on this in the next lesson)
- Convenience helpers —
res.json(),res.send(),res.status(), and more
Express deliberately stays small. It gives you the rails, not the whole train.
Express is like a post office sorting room
Imagine all incoming mail arriving in one giant pile (Node's raw http server). Without Express, you personally dig through every envelope to decide what goes where. With Express, there is a sorting room with labeled slots — one for letters addressed to /users, one for /products, one for /login. Each piece of mail lands in the right slot automatically, and you only write the handler for that slot.
#Installing Express
# Create a project folder and initialise it
mkdir my-server && cd my-server
npm init -y
# Install Express
npm install express#Your First Express Server
// server.js
const express = require("express");
const app = express(); // create the application
app.get("/", (req, res) => {
res.send("Hello from Express!");
});
app.listen(3000, () => {
console.log("Server running at http://localhost:3000");
});Notice how much cleaner this is compared to the raw http version. app.get tells Express: "when someone sends a GET request to /, run this function." The function receives two objects — req (the incoming request) and res (the outgoing response) — and you call methods on res to send something back.
#GET and POST: The Two Routes You'll Use Most
HTTP has several methods (also called verbs) that describe what a client wants to do. The two you'll reach for constantly are:
- GET — retrieve data (loading a page, fetching a list of items)
- POST — send data to the server (submitting a form, creating a new record)
Express has a method for each: app.get() and app.post().
app.use(express.json()); // allow Express to read JSON request bodies
app.get("/users", (req, res) => {
// Respond with a JSON array
res.json([{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]);
});
app.post("/users", (req, res) => {
const newUser = req.body; // the JSON the client sent
console.log("Creating user:", newUser);
res.status(201).json({ created: true, user: newUser });
});What is res.json vs res.send?
res.send() is the general-purpose response helper — it accepts a string, a Buffer, or an object. res.json() is a specialised version that always serialises its argument to JSON and sets the Content-Type header to application/json automatically. When you are building an API, prefer res.json() so clients can rely on the content type being correct.
#Route Parameters: Dynamic URLs with :id
Real applications rarely serve just static paths. You need routes like /users/42 or /products/t-shirt-blue where part of the URL is variable. Express handles this with route parameters — segments prefixed with a colon.
Whatever appears in that position in the URL gets captured and placed on the req.params object.
app.get("/users/:id", (req, res) => {
const userId = req.params.id; // captured from the URL
console.log("Requested user ID:", userId);
// In a real app you'd look this up in a database
res.json({ id: userId, name: "Alice" });
});Route params are always strings
Even if the URL is /users/42, req.params.id is the string "42", not the number 42. If you need to use it as a number — for example, to look something up in an array by index — convert it first: const id = Number(req.params.id). Comparing "42" === 42 will always be false and can cause silent, baffling bugs.
#Putting It All Together
const express = require("express");
const app = express();
app.use(express.json());
const books = [
{ id: 1, title: "Dune" },
{ id: 2, title: "Neuromancer" },
];
app.get("/books", (req, res) => {
res.json(books);
});
app.get("/books/:id", (req, res) => {
const id = Number(req.params.id);
const book = books.find((b) => b.id === id);
if (!book) {
return res.status(404).json({ error: "Book not found" });
}
res.json(book);
});
app.post("/books", (req, res) => {
const newBook = { id: books.length + 1, ...req.body };
books.push(newBook);
res.status(201).json(newBook);
});
app.listen(3000, () => console.log("Listening on port 3000"));Look at how each route reads almost like a sentence: "when someone GETs /books, send back all books"; "when someone GETs /books/:id, find that book and send it"; "when someone POSTs to /books, add the new one and return it." This is why Express became so popular — it brings your server's structure to the surface.
A client sends a GET request to `/products/shirt-blue`. Your route is `app.get("/products/:slug", ...)`. What is `req.params.slug`?
Use app.listen last, define routes first
Express registers routes in the order they appear in your file. app.listen simply starts accepting connections — it does not block or reorder anything. The convention is to define all your routes first, then call app.listen at the very end. That way the file reads top-to-bottom like a menu: here are the things this server can do, and here is where it opens its doors.
Key takeaways
- Express wraps Node's built-in HTTP module to give you clean, readable routing instead of one giant if-chain.
- `app.get()` and `app.post()` map an HTTP method plus a URL path to a handler function.
- Route parameters like `:id` capture variable URL segments onto `req.params` — always as strings.
- `res.json()` serialises data to JSON and sets the correct Content-Type header; prefer it over `res.send()` when building APIs.
- `res.status(code)` lets you set HTTP status codes like 201 (Created) or 404 (Not Found) before sending the response.
A client sends a GET request to /users/42. What does this route print to the console?
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
console.log(userId);
console.log(typeof userId);
res.json({ id: userId });
});This route should return the book whose id matches the URL, but it always returns 404 even for /books/1. What's wrong?
const books = [{ id: 1, title: "Dune" }];
app.get("/books/:id", (req, res) => {
const id = req.params.id;
const book = books.find((b) => b.id === id);
if (!book) {
return res.status(404).json({ error: "Book not found" });
}
res.json(book);
});Complete the response so it sends the user object back as JSON with an HTTP 201 (Created) status code.
app.post("/users", (req, res) => { const newUser = req.body; res.status(201).(newUser); });
Put these lines in the right order to build a minimal Express server that responds on the / route.
app.get("/", (req, res) => res.send("Hello from Express!"));const express = require("express");const app = express();
app.listen(3000, () => console.log("Server running"));Build a tiny Express API for a to-do list. It needs three routes: GET /todos returns all todos as JSON; GET /todos/:id returns a single todo by its numeric id, or a 404 JSON error if not found; POST /todos reads req.body.task and adds a new todo to the list, responding with 201 and the created todo. Start with the two seed todos already in the starter code.
Try it yourself — a starting point to build on:
const express = require("express");
const app = express();
app.use(express.json());
const todos = [
{ id: 1, task: "Learn Express" },
{ id: 2, task: "Build an API" },
];
// TODO: GET /todos — respond with the full todos array
// TODO: GET /todos/:id — find by numeric id, return 404 if missing
// TODO: POST /todos — read req.body.task, push a new todo, respond 201
app.listen(3000, () => console.log("Listening on port 3000"));