Introduction to Express.js

Express is a small, unopinionated web framework for Node.js. It gives you routing, middleware and helpers for requests and responses, and leaves everything else (database, templates, auth) up to you. These examples target Express 5, which needs Node.js 18 or later.

1. Installation

Create a project folder and install Express:

mkdir my-app
cd my-app
npm init -y
npm install express

2. Hello world

Create app.js:

const express = require('express');

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello, world!');
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

Run it and open http://localhost:3000 in your browser:

node app.js

Tip: node --watch app.js restarts the server automatically when you save a file.

3. Routing

A route is an HTTP method, a path and a handler function. Express has a method for each HTTP verb.

app.get('/users', (req, res) => res.send('List users'));
app.post('/users', (req, res) => res.send('Create user'));
app.put('/users/:id', (req, res) => res.send('Replace user'));
app.patch('/users/:id', (req, res) => res.send('Update user'));
app.delete('/users/:id', (req, res) => res.send('Delete user'));

Route parameters

Segments starting with : are captured in req.params.

// GET /users/42/posts/7
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params; // { userId: '42', postId: '7' }
  res.send(`Post ${postId} by user ${userId}`);
});

Query strings

// GET /search?q=express&page=2
app.get('/search', (req, res) => {
  const { q, page = 1 } = req.query;
  res.send(`Searching for "${q}", page ${page}`);
});

Chaining routes on one path

app.route('/books')
  .get((req, res) => res.send('Get all books'))
  .post((req, res) => res.send('Add a book'));

4. Request and response

Every handler receives a request (req) and a response (res) object.

Property / methodWhat it does
req.paramsRoute parameters, e.g. :id
req.queryParsed query string
req.bodyRequest body (needs a body-parsing middleware)
req.get('Header')Read a request header
res.send(data)Send a string, Buffer or object
res.json(obj)Send JSON
res.status(code)Set the status code (chainable)
res.redirect(url)Redirect the client
res.sendFile(path)Send a file (absolute path)
app.get('/api/status', (req, res) => {
  res.status(200).json({ ok: true, time: new Date() });
});

app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page');
});

5. Middleware

Middleware functions run in order between the incoming request and the final response. Each one can read or change req and res, end the request, or call next() to pass control on.

A custom logger

function logger(req, res, next) {
  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
  next(); // without this, the request hangs
}

app.use(logger); // runs for every request

Built-in middleware

app.use(express.json());                          // parse JSON bodies into req.body
app.use(express.urlencoded({ extended: true }));  // parse HTML form bodies
app.use(express.static('public'));                // serve files from ./public

With express.static('public'), a file at public/style.css is served at /style.css.

Middleware on a single route

function requireApiKey(req, res, next) {
  if (req.get('x-api-key') !== 'secret') {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
}

app.get('/admin', requireApiKey, (req, res) => {
  res.send('Welcome, admin');
});

Third-party middleware

Popular packages plug in the same way:

npm install cors helmet morgan
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

app.use(helmet());       // security headers
app.use(cors());         // allow cross-origin requests
app.use(morgan('dev'));  // request logging

6. Modular routes with Router

As your app grows, group related routes into their own files with express.Router().

routes/users.js:

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => res.send('All users'));
router.get('/:id', (req, res) => res.send(`User ${req.params.id}`));

module.exports = router;

app.js:

const usersRouter = require('./routes/users');

app.use('/users', usersRouter); // GET /users and GET /users/:id

7. Error handling

An error-handling middleware has four arguments. Register it after all your routes.

app.get('/boom', (req, res) => {
  throw new Error('Something broke');
});

// 404 for anything that didn't match a route
app.use((req, res) => {
  res.status(404).json({ error: 'Not found' });
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({ error: err.message });
});

Async handlers

In Express 5, if an async handler throws or returns a rejected promise, the error is passed to your error handler automatically. No try/catch needed just to call next(err).

app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id); // if this rejects, the error handler runs
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

8. Putting it together: a small REST API

A complete in-memory to-do API in one file:

const express = require('express');

const app = express();
app.use(express.json());

let todos = [
  { id: 1, title: 'Learn Express', done: false },
];
let nextId = 2;

// List all
app.get('/todos', (req, res) => {
  res.json(todos);
});

// Get one
app.get('/todos/:id', (req, res) => {
  const todo = todos.find(t => t.id === Number(req.params.id));
  if (!todo) return res.status(404).json({ error: 'Not found' });
  res.json(todo);
});

// Create
app.post('/todos', (req, res) => {
  const { title } = req.body;
  if (!title) return res.status(400).json({ error: 'title is required' });

  const todo = { id: nextId++, title, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

// Update
app.patch('/todos/:id', (req, res) => {
  const todo = todos.find(t => t.id === Number(req.params.id));
  if (!todo) return res.status(404).json({ error: 'Not found' });

  Object.assign(todo, req.body);
  res.json(todo);
});

// Delete
app.delete('/todos/:id', (req, res) => {
  const before = todos.length;
  todos = todos.filter(t => t.id !== Number(req.params.id));
  if (todos.length === before) return res.status(404).json({ error: 'Not found' });
  res.status(204).end();
});

app.listen(3000, () => console.log('API on http://localhost:3000'));

Try it with curl

curl http://localhost:3000/todos

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Write docs"}'

curl -X PATCH http://localhost:3000/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"done":true}'

curl -X DELETE http://localhost:3000/todos/1

9. Next steps

Using ES modules? Add "type": "module" to package.json and replace const express = require('express') with import express from 'express'.