Express.js

Node.js minimal web framework - unopinionated, middleware-based, the industry standard for Node backends

TL;DR

One-liner: Express is Node.js’s minimal web framework - build servers with just the pieces you need.

Core Strengths:

  • Minimal and unopinionated - you decide the architecture
  • Middleware ecosystem - thousands of plugins
  • Industry standard - most popular Node.js framework
  • Express 5 - native promise support, better security

Core Concepts

Concept 1: Middleware

Everything in Express is middleware - functions that have access to request, response, and next.

// Middleware runs in order
app.use(express.json());      // 1. Parse JSON
app.use(logRequest);          // 2. Log
app.get('/api', handler);     // 3. Route handler

function logRequest(req, res, next) {
  console.log(`${req.method} ${req.path}`);
  next();  // Pass to next middleware
}

Concept 2: Routing

Define endpoints with HTTP methods:

app.get('/users', getUsers);      // GET /users
app.post('/users', createUser);   // POST /users
app.put('/users/:id', updateUser); // PUT /users/123
app.delete('/users/:id', deleteUser);

Concept 3: Request & Response

app.get('/users/:id', (req, res) => {
  const id = req.params.id;       // URL params
  const sort = req.query.sort;    // Query string ?sort=name
  const token = req.headers.authorization;

  res.status(200).json({ id, sort });
});

Quick Start

Create Project

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

Create index.js

import express from 'express';
const app = express();

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Hello Express!' });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Run

node index.js
# Open http://localhost:3000

Gotchas

Don’t forget next() in middleware

// ❌ Request hangs forever
app.use((req, res, next) => {
  console.log('Logging...');
  // Missing next()!
});

// ✅ Correct
app.use((req, res, next) => {
  console.log('Logging...');
  next();
});

Error handling needs 4 parameters

// Must have all 4 params for Express to recognize it
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something broke!' });
});

// Trigger errors with next(err)
app.get('/fail', (req, res, next) => {
  next(new Error('Oops!'));
});

Route order matters

// ❌ Wrong - /users/me never matches
app.get('/users/:id', (req, res) => ...);
app.get('/users/me', (req, res) => ...);  // Never reached!

// ✅ Correct - specific routes first
app.get('/users/me', (req, res) => ...);
app.get('/users/:id', (req, res) => ...);

Async errors in Express 5+

// Express 5: async errors are caught automatically
app.get('/data', async (req, res) => {
  const data = await fetchData();  // Errors caught!
  res.json(data);
});

// Express 4: need try-catch or wrapper
app.get('/data', async (req, res, next) => {
  try {
    const data = await fetchData();
    res.json(data);
  } catch (err) {
    next(err);
  }
});

When to Use

Best for:

  • REST APIs
  • Lightweight servers
  • Teams wanting full control
  • Projects with specific architecture needs

Not ideal for:

  • Large apps needing structure (use NestJS)
  • Real-time apps (use Fastify or Hono)
  • TypeScript-first projects (use NestJS)

Comparison:

FeatureExpressFastifyNestJS
SpeedModerateFastModerate
OpinionatedNoNoYes
TypeScriptAdd-onBuilt-inBuilt-in
Learning curveEasyEasyMedium

Next Steps

Cheatsheet

PatternCode
GET routeapp.get('/path', handler)
POST routeapp.post('/path', handler)
Middlewareapp.use(middleware)
URL paramreq.params.id
Query paramreq.query.name
Bodyreq.body (need express.json())
JSON responseres.json({ data })
Statusres.status(404).json({})
Routerconst router = express.Router()
Static filesapp.use(express.static('public'))