Node's built-in http module can run a web server, but as the previous lesson showed, even simple tasks such as routing, reading JSON bodies and setting headers take a lot of code. Express is a small, flexible framework that handles all of that for you. It's been the most popular Node.js web framework for over a decade, and the ideas you learn with it carry straight over to alternatives such as Fastify, Koa and Hono.
With Express, you create an app, then describe routes: "when a GET request arrives at /products, run this function". Express parses the URL, extracts parameters like the 42 in /products/42, reads the query string, and gives you helpful methods like res.json() and res.status() to build the response.
In this lesson you'll install Express 5, create your first app, define routes for different HTTP methods, read route parameters, query strings and JSON bodies, and send responses with the right status codes. The following lessons build on this with routers, middleware and a complete REST API.
Installing and starting Express
npm init -y
npm install expressimport express from 'express';
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
app.listen(3000, () => console.log('http://localhost:3000'));express()creates an application.app.get(path, handler)handles GET requests to that path. The handler receives Express's enhancedreqandresobjects.res.sendsends text or HTML and sets the content type for you.- Run with
node --watch server.jsso the server restarts whenever you save a file.
Routes, parameters and query strings
app.get('/products', (req, res) => {
const { search = '', page = '1' } = req.query; // /products?search=lamp&page=2
res.json({ search, page: Number(page) });
});
app.get('/products/:id', (req, res) => {
const id = Number(req.params.id); // /products/42
res.json({ id });
});- Route parameters start with a colon (
:id) and appear inreq.params. - Query string values appear in
req.query. Both are always strings, so convert numbers yourself. - There's a method for each HTTP verb:
app.get,app.post,app.put,app.patchandapp.delete. - Express checks routes in the order they're defined and uses the first match.
JSON bodies and responses
To read JSON sent by clients, add the built-in body parser once, before your routes:
app.use(express.json());
app.post('/orders', (req, res) => {
const { item, qty } = req.body;
res.status(201).json({ id: 1, item, qty });
});| Method | Sends |
|---|---|
res.send(text) | Text or HTML |
res.json(data) | JSON, with the right header |
res.status(code) | Sets the status; chain it: res.status(404).json(...) |
res.sendStatus(code) | Just a status, such as 204 |
res.redirect(url) | A redirect, 302 by default |
res.set(name, value) | A response header |
Send exactly one response per request. Calling res.json twice causes an "headers already sent" error.
Request details and a fallback for unknown routes
Besides params, query and body, Express's req gives you tidy access to the rest of the request. For a request to /info/7?x=1:
| Property or method | Gives |
|---|---|
req.get('user-agent') | A request header, matched case-insensitively |
req.path | '/info/7', the path without the query string |
req.originalUrl | '/info/7?x=1', the full URL as requested |
req.hostname | The host name, such as 'localhost' |
req.ip | The client's IP address |
req.is('json') | A truthy value if the request body is JSON, otherwise false or null |
On the response side, res.type('csv') sets the Content-Type from a short name, so res.type('csv').send('a,b\n1,2') is served as text/csv.
If no route matches, Express sends a plain "Cannot GET /nope" page with status 404. APIs usually replace it with JSON by adding one last app.use after all the routes:
app.use((req, res) => {
res.status(404).json({ error: `No route for ${req.method} ${req.path}` });
});Because Express tries routes in order, this function only runs when nothing above it sent a response. You'll also notice Express answers HEAD requests for any GET route automatically, sending the headers without the body.
Node.js Web Syntax
Express essentials
import express from 'express';
const app = express();
app.use(express.json());
app.get('/items', (req, res) => res.json(items));
app.get('/items/:id', (req, res) => res.json({ id: req.params.id }));
app.post('/items', (req, res) => res.status(201).json(req.body));
app.delete('/items/:id', (req, res) => res.sendStatus(204));
app.get('/old', (req, res) => res.redirect(301, '/new'));
app.listen(process.env.PORT ?? 3000);Node.js Web Summary
- Express is a minimal web framework built on Node's http module.
- app.get, app.post, app.put, app.patch and app.delete define routes for each method.
- req.params holds :route parameters; req.query holds the query string; both are strings.
- app.use(express.json()) parses JSON bodies into req.body.
- res.json, res.send, res.status and res.sendStatus build responses; send exactly one.
Node.js Web Examples
Each example shows the code, the output it produces and what each part does.
A first Express app
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.send('<h1>Welcome to the shop</h1>');
});
app.get('/hello/:name', (req, res) => {
const times = Number(req.query.times ?? 1);
res.send(`Hello ${req.params.name}! `.repeat(times).trim());
});
const server = app.listen(0, async () => {
const base = `http://localhost:${server.address().port}`;
for (const path of ['/', '/hello/Ada', '/hello/Sam?times=2', '/missing']) {
const r = await fetch(base + path);
console.log(r.status, r.headers.get('content-type').split(';')[0], r.ok ? await r.text() : '(default 404 page)');
}
server.close();
});200 text/html <h1>Welcome to the shop</h1>
200 text/html Hello Ada!
200 text/html Hello Sam! Hello Sam!
404 text/html (default 404 page)Line-by-line explanation
- Lines 5 to 7res.send sets the Content-Type header from the content automatically.
- Line 9:name is a route parameter, available as req.params.name.
- Line 10Query values are strings, so times is converted to a number.
- Line 14Port 0 picks an unused port; the demo calls its own routes with fetch.
- Line 16Paths with no matching route get Express's default 404 page.
JSON in and out
import express from 'express';
const app = express();
app.use(express.json());
const products = [
{ id: 1, name: 'Mug', price: 8.5 },
{ id: 2, name: 'Lamp', price: 24 },
];
app.get('/products', (req, res) => {
const max = req.query.maxPrice ? Number(req.query.maxPrice) : Infinity;
res.json(products.filter((p) => p.price <= max));
});
app.get('/products/:id', (req, res) => {
const product = products.find((p) => p.id === Number(req.params.id));
if (!product) return res.status(404).json({ error: 'Product not found' });
res.json(product);
});
app.post('/products', (req, res) => {
const product = { id: products.length + 1, name: req.body.name, price: req.body.price };
products.push(product);
res.status(201).json(product);
});
const server = app.listen(0, async () => {
const base = `http://localhost:${server.address().port}`;
const show = async (r) => console.log(r.status, JSON.stringify(await r.json()));
await show(await fetch(`${base}/products?maxPrice=10`));
await show(await fetch(`${base}/products/7`));
await show(await fetch(`${base}/products`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Pen', price: 1.2 }),
}));
console.log(products.length);
server.close();
});200 [{"id":1,"name":"Mug","price":8.5}]
404 {"error":"Product not found"}
201 {"id":3,"name":"Pen","price":1.2}
3Line-by-line explanation
- Line 4express.json() parses JSON request bodies into req.body.
- Line 12A query parameter filters the list when it's present.
- Line 18return stops the handler after sending the 404, so only one response is sent.
- Lines 22 to 26The parsed body is available as a plain object.
- Line 25status and json chain together.
Route order and other responses
import express from 'express';
const app = express();
app.get('/users/me', (req, res) => res.json({ user: 'current user' }));
app.get('/users/:id', (req, res) => res.json({ user: `user ${req.params.id}` }));
app.get('/old-pricing', (req, res) => res.redirect(301, '/pricing'));
app.delete('/sessions/current', (req, res) => res.sendStatus(204));
app.get('/download', (req, res) => {
res.set('Content-Disposition', 'attachment; filename="report.csv"');
res.type('text/csv').send('month,orders\nMarch,1204\n');
});
const server = app.listen(0, async () => {
const base = `http://localhost:${server.address().port}`;
console.log(await (await fetch(`${base}/users/me`)).text());
console.log(await (await fetch(`${base}/users/42`)).text());
const moved = await fetch(`${base}/old-pricing`, { redirect: 'manual' });
console.log(moved.status, moved.headers.get('location'));
console.log((await fetch(`${base}/sessions/current`, { method: 'DELETE' })).status);
const file = await fetch(`${base}/download`);
console.log(file.headers.get('content-disposition'));
server.close();
});{"user":"current user"}
{"user":"user 42"}
301 /pricing
204
attachment; filename="report.csv"Line-by-line explanation
- Lines 5 to 6/users/me is defined first; if /users/:id came first, it would catch "me" as an id.
- Line 7redirect sends a 301 with a Location header.
- Line 8sendStatus sends just a status code, ideal for successful deletes.
- Lines 10 to 11res.set adds a header and res.type sets the content type.
- Line 18redirect: 'manual' lets the demo see the redirect instead of following it.
Common Node.js Web Mistakes
Forgetting express.json()
app.post('/orders', (req, res) => {
console.log(req.body.item); // TypeError: req.body is undefined
});app.use(express.json());
app.post('/orders', (req, res) => {
console.log(req.body.item);
});Sending two responses
app.get('/item/:id', (req, res) => {
if (!found) res.status(404).json({ error: 'Not found' });
res.json(found); // Error: Cannot set headers after they are sent
});app.get('/item/:id', (req, res) => {
if (!found) return res.status(404).json({ error: 'Not found' });
res.json(found);
});Treating params as numbers
const product = products.find((p) => p.id === req.params.id); // 2 === "2" is falseconst product = products.find((p) => p.id === Number(req.params.id));Node.js Web in Real Projects
Express in industry
Express powers APIs at companies of every size, from start-ups to large enterprises, and it's the base of frameworks such as NestJS. A typical project has an app.js that sets up middleware and routers, and a server.js that calls listen, so tests can import the app without starting a real server. Hosting platforms provide the port through process.env.PORT.
Node.js Web FAQ
What is the difference between req.params, req.query and req.body?
req.params holds values from the path, such as id in /products/:id. req.query holds the query string after the ?, such as ?page=2. req.body holds data sent in the request body, usually JSON from a POST or PUT, and is only filled in when a body parser such as express.json() has run.
Why is req.body undefined in Express?
Express doesn't read request bodies by default. Add app.use(express.json()) before your routes, and make sure the client sends a Content-Type: application/json header, because the parser skips anything else. For HTML form posts, add express.urlencoded() as well.
What is the difference between res.send and res.json?
res.json(data) always converts the value to JSON and sets Content-Type: application/json. res.send looks at what you give it: a string is sent as HTML, a Buffer as binary data, and an object or array is passed to res.json. Use res.json for APIs so the intent is clear, even when the value happens to be a string.
Should I use Express 4 or Express 5?
Use Express 5 for new projects. It's the current major version, catches errors from async handlers automatically, and drops some old methods and path syntax. Express 4 code mostly runs unchanged, but when upgrading, check route patterns that use * or ? and replace res.send(404), which now sends the text "404" with a 200 status, with res.sendStatus(404).
How do I make an Express app listen on the port my host provides?
Read the port from an environment variable with a default for development: const port = Number(process.env.PORT ?? 3000) and app.listen(port). Platforms such as Render, Railway and Heroku set PORT for you and route traffic to it. Hard-coding a port often makes the app unreachable after deployment.
Node.js Web Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the route so it handles GET requests to /about.
app.('/about', (req, res) => res.send('About us'));
Where does Express put the value of :id in the route /orders/:id?
A client requests /search?q=lamp&page=2. What is typeof req.query.page?
What does app.use(express.json()) do?
Why should /users/me be defined before /users/:id?
Node.js Web Quiz
5 questions to check what you learned.
Which method sends JSON with the right Content-Type?
- res.send
- res.write
- res.end
- res.json
How do you send a 404 with a JSON message?
- res.json(404, {...})
- res.status(404).json({ error: 'Not found' })
- res.send404()
- res.error(404)
What error appears if a handler sends two responses?
- Cannot set headers after they are sent
- 404
- Unexpected token
- ECONNRESET
Which response suits a successful DELETE with nothing to return?
- res.json(null)
- res.redirect('/')
- res.status(500)
- res.sendStatus(204)
What does Express 5 do differently with errors thrown in async route handlers?
- Ignores them
- Restarts the server
- Passes them to the error-handling middleware automatically
- Returns 200