Skip to main content

What is Express.js and why is it used?

Express.js is a minimal web framework for Node.js that adds routing, middleware, and HTTP utilities on top of Node.js's built-in http module.

Theory

TL;DR

  • Express is to Node.js what a prep station is to a raw kitchen: Node gives you the stove and ingredients, Express gives you knives, cutting boards, and recipes
  • Main difference: 15+ lines of manual request parsing in Node.js become 3 declarative lines in Express
  • Express automatically handles URL routing, JSON parsing, and response headers
  • Decision rule: use Express for any Node.js web app unless you need zero dependencies or are running at 10k+ req/s (then Fastify)

Quick example

js
// Pure Node.js: manual routing, manual headers, manual everything const http = require('http'); const server = http.createServer((req, res) => { if (req.url === '/users' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify([{ id: 1, name: 'Alice' }])); } else { res.writeHead(404); res.end('Not Found'); } }); server.listen(3000); // Express: same result, 3 lines const express = require('express'); const app = express(); app.get('/users', (req, res) => res.json([{ id: 1, name: 'Alice' }])); app.listen(3000);

Both produce identical responses. Express removes all string matching on req.url and manual header management.

What Express actually does

Node.js's http module gives you a raw request object: a URL string, a method string, and a body as a buffer. You parse all of it yourself. Express wraps that object with a router (a radix tree for fast path lookup), attaches parsers via middleware, and gives you res.json(), res.status(), and similar helpers. You write route handlers instead of if/else chains.

The middleware stack is the core idea. Every app.use() call adds a function to a queue. When a request comes in, Express runs each function in order, passing req and res through. If a middleware calls next(), the next function runs. If it calls res.send(), the chain stops there.

When to use Express

  • REST API or web app: Express is the default pick
  • Prototyping: fastest path from idea to running server
  • Full-stack app with auth and a database: Express plus a middleware stack (cors, helmet, body-parser)
  • Serverless or edge functions with many routes: Express works; under 10 routes, hono.js is lighter
  • Microservice with a custom protocol: skip Express, use http directly
  • 10k+ requests per second in production: Fastify is about 2x faster; Express handles up to ~5k req/s comfortably

Alternatives at a glance

FrameworkStyleBest for
ExpressMinimal, flexibleREST APIs, full-stack apps
FastifyFast, schema-basedHigh-throughput APIs
KoaAsync-first, smaller coreGreenfield projects, custom middleware
NestJSOpinionated, TypeScriptEnterprise apps with structure
hono.jsTiny, edge-readyServerless, Cloudflare Workers

Koa is worth knowing for interviews. It uses async/await natively where Express uses callbacks, and its core is much smaller. For new projects, Koa is a valid pick. When a large plugin ecosystem matters, Express wins.

Common mistakes

Missing express.json() before routes. This breaks around 40% of first Express apps. Without the middleware, req.body is undefined and any JSON access throws.

js
// Wrong: req.body is undefined app.post('/user', (req, res) => res.send(req.body.name)); // TypeError // Correct: parse JSON globally before any route app.use(express.json()); app.post('/user', (req, res) => res.send(req.body.name)); // "Alice"

Wrong middleware order. Middleware runs top to bottom. Register a route before express.json() and that route sees an unparsed body.

js
// Wrong: route runs before parser app.post('/api/user', (req, res) => res.send(req.body.name)); // undefined app.use(express.json()); // too late // Correct app.use(express.json()); // must be first app.post('/api/user', (req, res) => res.send(req.body.name)); // works

Calling res.send() twice. Node throws "Cannot set headers after they are sent". Fix with an early return.

js
// Wrong: two sends in the same handler if (err) res.status(500).send('Error'); res.json({ ok: true }); // crash // Correct if (err) return res.status(500).send('Error'); res.json({ ok: true });

No next() in middleware. The request hangs. No response reaches the client, timeouts fire in production monitors.

js
// Wrong: request hangs forever app.use((req, res, next) => { console.log(req.method); // forgot next() }); // Correct app.use((req, res, next) => { console.log(req.method); next(); });

Real-world usage

  • Strapi: headless CMS with 1M+ downloads, built on Express for REST endpoints and admin panel
  • Ghost: blogging platform, Express handles dynamic routes and themes
  • PayloadCMS: Express plus TypeScript for e-commerce backends, 10k+ developers
  • Next.js API routes: uses an Express-like router internally for /pages/api handlers
  • Most internal Node.js tools at companies start with Express and only move to Fastify when throughput becomes the actual problem

I've seen teams use Express for years without hitting its performance ceiling, because the real bottleneck is usually the database query. If a query takes 50ms, Express overhead of 0.1ms does not matter.

Follow-up questions

Q: What is middleware in Express and how does it work?
A: A middleware is a function with signature (req, res, next). Express calls them in order for every request. You use them to parse bodies, check auth, log requests, or attach data to req. If a middleware skips calling next(), the chain stops and the client gets no response.

Q: How does Express handle async errors?
A: It does not automatically catch them. If you throw inside an async route handler, Express won't pick it up. Wrap every handler in try/catch and call next(err), or use the express-async-errors package which patches the router to catch Promise rejections.

Q: What is the difference between app.get() and router.get()?
A: app is the global Express instance. router is a mini Express app you can mount at a path. Use router to group routes by prefix: app.use('/api', router) mounts all router routes under /api. This keeps large codebases modular without one giant file.

Q: How does Express routing scale to hundreds of routes?
A: Express uses a radix tree for route matching. Lookup is O(k) where k is path length, not O(n) routes. So 1000 routes do not slow down matching compared to 10 routes.

Q: Express vs Koa: when would you pick Koa?
A: Koa uses async/await natively and has a smaller core with no built-in middleware. Good pick for new projects where you want full control over the stack. Express has a larger plugin ecosystem and more community examples. Teams on existing Node.js codebases usually stay with Express because switching costs outweigh the benefits.

Examples

Basic REST API with JSON body parsing

js
const express = require('express'); const app = express(); app.use(express.json()); // parse JSON request bodies app.get('/users', (req, res) => { res.json([{ id: 1, name: 'Alice' }]); }); app.post('/users', (req, res) => { const { name } = req.body; // works because of express.json() res.status(201).json({ id: 2, name }); }); app.listen(3000, () => console.log('Running on 3000'));

app.use(express.json()) runs before every route. A POST to /users with a JSON body gets req.body automatically populated. Without that line, req.body is undefined and the destructuring crashes.

Webhook handler with middleware order (production pattern)

This is how Stripe webhook verification looks in a real Express app:

js
const express = require('express'); const app = express(); // Raw buffer needed for signature check - must come before express.json() app.use('/webhook/stripe', express.raw({ type: 'application/json' })); app.use(express.json()); // JSON parsing for all other routes app.post('/webhook/stripe', (req, res) => { const sig = req.headers['stripe-signature']; if (!verifySignature(sig, req.body)) { return res.status(400).send('Invalid signature'); } console.log('Event:', req.body.type); // 'payment_intent.succeeded' res.json({ received: true }); }); app.listen(3000);

The /webhook/stripe route needs the raw buffer to verify the signature. Everything else gets parsed JSON. Middleware order controls which parser runs first, so the specific route middleware is registered before the global one. This is a real pattern from projects like PayloadCMS.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?