Overview

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.

Installing Node & Express

Prerequisites

Express v5 requires Node 18+ (or newer); older runtimes are no longer supported . Check your version:

$ node --version
v20.11.1

Quick Start

# 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

Hello World

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.

Project Structure

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.

Core Concepts

Routing

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

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

Request & Response Helpers

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 });

Template Engines & Views

Express is agnostic—choose any engine:

EngineInstallConfigure
Pugnpm i pug app.set('view engine','pug');
EJSnpm i ejs app.set('view engine','ejs');
Handlebarsnpm i express‑handlebars app.engine('hbs', hbs({ … }));

Render a view:

app.get('/hello', (req, res) => {
  res.render('greeting', { name: 'Friend' });
});

Static Assets

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.

Error Handling

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.

Security Best Practices

Performance & Scaling

Building REST APIs

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
});

Authentication & Authorization

Testing & Quality Assurance

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.

Deployment

Migrating from Express 4 → 5

  1. Update package.json: "express": "^5"
  2. Replace deprecated body‑parser with built‑ins.
  3. Wrap legacy callbacks in util.promisify() or refactor to async.
  4. Install official codemod tool (npx express‑codemod) to bulk‑update route handlers .
  5. Add a global error handler if missing—v5 routes unhandled rejections there automatically.

Advanced Patterns & Integrations

Further Resources