Express is a minimal, unopinionated web‑application framework that sits atop Node .js and
provides robust tooling for building RESTful APIs, server‑side rendered (SSR) sites, and real‑time
services. As of the latest LTS release is
v5.1.0.
The v5 line brings native async/await
support, promise‑aware error handling, stricter routing, and
improved TypeScript typings.
Tip: Express delivers just the HTTP layer—everything else (validation, authentication, database access) is added through middleware. This keeps the core small and lets you compose your application à la carte.
Express v5 requires Node 18+ (or newer); older runtimes are no longer supported . Check your version:
$ node --version
v20.11.1
# 1 Create a new project folder
mkdir my‑express‑app && cd $_
# 2 Initialise npm
npm init -y
# 3 Install Express
npm install express
# 4 Create an entry script
touch index.js
import express from 'express'; // ES Modules syntax
const app = express();
const port = 3000;
app.get('/', (req, res) => { // app.get()
res.send('Hello Express 5 👋');
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
Note: Add "type": "module"
to package.json
for the
import
syntax, or switch to require()
for CommonJS.
Express does not dictate structure, but a common MVC layout keeps things tidy:
my-express-app/
├── src/
│ ├── index.js # entry
│ ├── routes/ # route definitions
│ ├── controllers/ # request handlers
│ ├── services/ # business logic
│ └── views/ # templates (EJS, Pug, etc.)
├── public/ # static assets
└── tests/ # unit & integration tests
Tip: Keep routes
thin—delegate heavy lifting to services
and controllers
to maintain separation of concerns.
Use app.METHOD() where METHOD
is an HTTP verb:
app.post('/users', (req, res) => { ... });
app.route('/books/:id')
.get(getBook) // show one
.put(updateBook) // replace
.delete(deleteBook); // remove
Middleware functions execute in order. Call next() to hand off control:
function logger(req, res, next){
console.time(req.method + ' ' + req.url);
res.on('finish', () => console.timeEnd(req.method + ' ' + req.url));
next(); // continue stack
}
app.use(logger); // application‑level MW
express.Router()
(err, req, res, next)
The req
and res
objects wrap Node’s native
IncomingMessage
/ServerResponse
with sugar:
req.params.id // URL : /book/:id
req.query.page // ?page=2
req.body.name // via express.json()
res.status(201).json({ id });
Express is agnostic—choose any engine:
Engine | Install | Configure |
---|---|---|
Pug | npm i pug |
app.set('view engine','pug'); |
EJS | npm i ejs |
app.set('view engine','ejs'); |
Handlebars | npm i express‑handlebars |
app.engine('hbs', hbs({ … })); |
Render a view:
app.get('/hello', (req, res) => {
res.render('greeting', { name: 'Friend' });
});
Serve any file in public/
through the built‑in
express.static() middleware:
app.use('/assets', express.static('public', {
maxAge: '7d', // cache control
setHeaders(res){ // custom headers
res.set('x-timestamp', Date.now());
}
}));
Note: Put static middleware near the top—before routes—to avoid unnecessary route matching.
With async/await you can simply throw—and Express 5 will catch and route to your global error handler:
app.get('/fail', async (req, res) => {
const user = await db.findUser(req.query.id);
if(!user) throw new Error('User not found');
res.json(user);
});
app.use(function(err, req, res, next){ // error‑handling MW
console.error(err.stack);
res.status(500).json({ message: err.message });
});
Tip: Never send stack traces to production—selectively reveal messages or IDs.
npm audit
& npm audit fix
regularly.helmet
to set CSP, HSTS, X‑Content‑Type‑Options headers.express‑rate‑limit
or a reverse‑proxy (Nginx / CloudFlare).xss‑clean
, express‑validator
).cors
middleware with explicit origins..env
in managed secret stores for production.compression()
(gzip/deflate).pm2 -i max
for multi‑core.Redis
or CDN edge caching.res.write()
) over buffering large files.clinic.js
, 0x
, Chrome DevTools Node profiling.app.use(express.json()); // JSON payloads
app.get('/api/v1/posts', listPosts);
app.post('/api/v1/posts', createPost);
app.patch('/api/v1/posts/:id', updatePost);
app.delete('/api/v1/posts/:id', deletePost);
Validate with joi
or zod
:
import { z } from 'zod';
const schema = z.object({
title: z.string().min(3),
body: z.string().max(2048)
});
app.post('/posts', (req, res, next) => {
const parse = schema.safeParse(req.body);
if(!parse.success) return res.status(400).json(parse.error);
// safe to use parse.data
});
express‑session
+ secure cookies.jsonwebtoken
) with
app.use(jwt()).function isAuthenticated(req, res, next){
if(req.user) return next();
res.status(401).send('Login required');
}
Write unit and integration tests with Jest
and SuperTest
:
import request from 'supertest';
import app from '../src/index.js';
test('GET / should return 200', async () => {
await request(app).get('/').expect(200);
});
Tip: Add npm run test -- --watch
for TDD red‑green‑refactor.
pm2
, systemd
, or Docker.npm ci
, tests, lint, and Docker publish.package.json
: "express": "^5"
body‑parser
with built‑ins.util.promisify()
or refactor to async
.npx express‑codemod
)
to bulk‑update route handlers .ws
or socket.io
side‑by‑side:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server: http.createServer(app) });
express‑graphql
or @apollo/server-express
.ts-node
, or build with tsc --outDir build
.@vendia/serverless-express
for
AWS Lambda.