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.
Create a project folder and install Express:
mkdir my-app
cd my-app
npm init -y
npm install express
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.
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'));
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}`);
});
// GET /search?q=express&page=2
app.get('/search', (req, res) => {
const { q, page = 1 } = req.query;
res.send(`Searching for "${q}", page ${page}`);
});
app.route('/books')
.get((req, res) => res.send('Get all books'))
.post((req, res) => res.send('Add a book'));
Every handler receives a request (req) and a response (res) object.
| Property / method | What it does |
|---|---|
req.params | Route parameters, e.g. :id |
req.query | Parsed query string |
req.body | Request 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');
});
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.
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
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.
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');
});
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
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
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 });
});
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);
});
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'));
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
zod or express-validator.app.set('view engine', ...).process.env), not in code.Using ES modules? Add "type": "module" to package.json and replace const express = require('express') with import express from 'express'.