Express Basics

Build web servers faster with Express: install it, create an app, define routes with app.get and app.post, read route and query parameters, parse JSON bodies, and send text, JSON and status codes.

Lesson 10 of 43Intermediate18 min read

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

Terminal
npm init -y
npm install express
JavaScript
import 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 enhanced req and res objects.
  • res.send sends text or HTML and sets the content type for you.
  • Run with node --watch server.js so the server restarts whenever you save a file.

Routes, parameters and query strings

JavaScript
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 in req.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.patch and app.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:

JavaScript
app.use(express.json());

app.post('/orders', (req, res) => {
  const { item, qty } = req.body;
  res.status(201).json({ id: 1, item, qty });
});
MethodSends
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 methodGives
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.hostnameThe host name, such as 'localhost'
req.ipThe 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:

JavaScript
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.

Example 1

A first Express app

Node.js
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();
});
Output
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
  1. Lines 5 to 7res.send sets the Content-Type header from the content automatically.
  2. Line 9:name is a route parameter, available as req.params.name.
  3. Line 10Query values are strings, so times is converted to a number.
  4. Line 14Port 0 picks an unused port; the demo calls its own routes with fetch.
  5. Line 16Paths with no matching route get Express's default 404 page.
Example 2

JSON in and out

Node.js
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();
});
Output
200 [{"id":1,"name":"Mug","price":8.5}]
404 {"error":"Product not found"}
201 {"id":3,"name":"Pen","price":1.2}
3
Line-by-line explanation
  1. Line 4express.json() parses JSON request bodies into req.body.
  2. Line 12A query parameter filters the list when it's present.
  3. Line 18return stops the handler after sending the 404, so only one response is sent.
  4. Lines 22 to 26The parsed body is available as a plain object.
  5. Line 25status and json chain together.
Example 3

Route order and other responses

Node.js
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();
});
Output
{"user":"current user"}
{"user":"user 42"}
301 /pricing
204
attachment; filename="report.csv"
Line-by-line explanation
  1. Lines 5 to 6/users/me is defined first; if /users/:id came first, it would catch "me" as an id.
  2. Line 7redirect sends a 301 with a Location header.
  3. Line 8sendStatus sends just a status code, ideal for successful deletes.
  4. Lines 10 to 11res.set adds a header and res.type sets the content type.
  5. Line 18redirect: 'manual' lets the demo see the redirect instead of following it.

Common Node.js Web Mistakes

Forgetting express.json()

Don't
app.post('/orders', (req, res) => {
  console.log(req.body.item);      // TypeError: req.body is undefined
});
Do
app.use(express.json());
app.post('/orders', (req, res) => {
  console.log(req.body.item);
});

Sending two responses

Don't
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
});
Do
app.get('/item/:id', (req, res) => {
  if (!found) return res.status(404).json({ error: 'Not found' });
  res.json(found);
});

Treating params as numbers

Don't
const product = products.find((p) => p.id === req.params.id);   // 2 === "2" is false
Do
const 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.

Fill in the blankseasy

Complete the route so it handles GET requests to /about.

app.('/about', (req, res) => res.send('About us'));
Multiple choiceeasy

Where does Express put the value of :id in the route /orders/:id?

Predict the outputmedium

A client requests /search?q=lamp&page=2. What is typeof req.query.page?

Multiple choicemedium

What does app.use(express.json()) do?

Multiple choicehard

Why should /users/me be defined before /users/:id?

Node.js Web Quiz

5 questions to check what you learned.

  1. Which method sends JSON with the right Content-Type?

    • res.send
    • res.write
    • res.end
    • res.json
  2. 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)
  3. What error appears if a handler sends two responses?

    • Cannot set headers after they are sent
    • 404
    • Unexpected token
    • ECONNRESET
  4. Which response suits a successful DELETE with nothing to return?

    • res.json(null)
    • res.redirect('/')
    • res.status(500)
    • res.sendStatus(204)
  5. 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