mrkeyoor.com_
Wed 05 Aug 05:01 UTC
npmWeb Backendupdated 05 Aug 2026

express

Express is the default web framework for Node.js: a thin layer of routing, middleware, and HTTP helpers over the built-in http module. You define routes with app.get and friends, chain middleware functions that each touch the request and pass it on, and render through any of 14+ template engines if you want server-side HTML. It is deliberately unopinionated: no ORM, no validation, no project structure. Version 5 is the current major, on Node 18+, and its headline change is that rejected promises in handlers now flow to your error middleware automatically. At roughly 128 million downloads a week it is still the framework every Node tutorial, Stack Overflow answer, and middleware package assumes.

Verdict

Still the safe default for a plain Node API or server-rendered app because the ecosystem and collective knowledge around it are unmatched. Pick Fastify for performance-sensitive JSON APIs and Hono for edge runtimes; pick Express when boring and well-documented is the feature.

API stability5/5The v4 API held stable for a decade; v5 is a mild, well-documented migration (route syntax, removed req.param) rather than a rewrite, and the middleware contract is unchanged.
Docs4/5expressjs.com covers the API and ships a proper v5 migration guide, but the wider web is full of outdated v4 tutorials that now teach broken route patterns, and the official guides stay shallow on production topics.
Maintenance4/5Active under an OpenJS-governed technical committee with recent pushes (Aug 2026) and 225 open issues and PRs, but the project moves deliberately slowly; v5 took about ten years to land.
Ecosystem5/5The largest middleware ecosystem in Node by a wide margin; the expressjs GitHub org maintains official middleware and nearly every third-party auth, logging, or upload package documents Express integration first.

Use it if

  • You want the framework with the most answers: nearly every Node hosting guide, auth tutorial, and middleware package targets Express first
  • Your team already knows it and you are hiring; Express experience is the closest thing Node has to a universal baseline
  • You depend on Express-specific middleware like Passport strategies, express-session, or multer that has no drop-in equivalent elsewhere
  • You want API stability measured in decades; the v4 API held for ten years and v5 keeps the same mental model
Skip it if

Setup reality

npm install express and you have a server in five lines; Node 18+ required, no config files. The real friction is elsewhere. Migrating v4 code to v5 means route pattern changes from path-to-regexp 8: string regex patterns are gone, wildcards must be named (/*splat instead of *), and optional params use braces. The mountains of v4-era tutorials online silently teach patterns that now throw. TypeScript users install @types/express separately and it occasionally lags releases. Middleware ordering bugs fail silently: register express.json() after your routes and req.body is just undefined. Error handlers are detected by having exactly four arguments, which trips people up constantly.

Patterns

Minimal HTTP serverbasic-server

import express from 'express'

const app = express()

app.get('/', (req, res) => {
  res.send('Hello World')
})

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

Express 5 requires Node 18+; ESM imports work out of the box, no transpiler needed.

Parse JSON request bodiesjson-body

app.use(express.json())

app.post('/api/items', (req, res) => {
  const { name } = req.body
  res.status(201).json({ name })
})

body-parser is built in since 4.16; default body limit is 100kb, raise it with express.json({ limit: '1mb' }). Register before your routes or req.body is undefined.

Read route and query parametersroute-params

app.get('/users/:id', (req, res) => {
  const { id } = req.params
  const { fields } = req.query
  res.json({ id, fields })
})

req.param() was removed in v5; always use req.params, req.query, or req.body explicitly.

Split routes into modules with Routermodular-router

// routes/users.js
import { Router } from 'express'
const router = Router()

router.get('/', (req, res) => res.json([]))
router.get('/:id', (req, res) => res.json({ id: req.params.id }))

export default router

// app.js
app.use('/users', router)

Paths inside the router are relative to the mount point, so router.get('/:id') serves /users/:id.

Write and apply middlewarecustom-middleware

function requestLogger(req, res, next) {
  console.log(`${req.method} ${req.originalUrl}`)
  next()
}

app.use(requestLogger)

Forgetting next() hangs the request forever with no error; middleware runs strictly in registration order.

Async handlers without try/catch wrappersasync-errors

app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id) // if this rejects,
  res.json(user) // Express 5 forwards the error to error middleware
})

This is the big v5 win: rejected promises reach your error handler automatically. In v4 the same code crashed or hung, which is why asyncHandler wrappers existed.

Central error-handling middlewareerror-handler

app.use((err, req, res, next) => {
  console.error(err)
  const status = err.status || 500
  res.status(status).json({ error: err.message })
})

Express identifies error middleware by its four-argument signature; drop next from the params and it silently becomes normal middleware. Register it last.

404 handler for unmatched routescatch-all-404

// after all other routes
app.use((req, res) => {
  res.status(404).json({ error: 'not found' })
})

If you need a path pattern instead, v5 wildcards must be named: app.all('/*splat', ...) replaces the bare '*' from v4.

Serve static filesstatic-files

import path from 'node:path'

app.use(express.static(path.join(import.meta.dirname, 'public'), {
  maxAge: '1d',
  index: 'index.html'
}))

Requests fall through to later routes on miss, so mount static before the 404 handler but after any API routes that share paths.

Enable CORS for an APIcors

import cors from 'cors'

app.use(cors({
  origin: 'https://app.example.com',
  credentials: true
}))

cors is a separate official package (npm i cors); with credentials: true you cannot use origin: '*', browsers will reject it.

Redirects and explicit status codesredirects-and-status

app.get('/old-path', (req, res) => {
  res.redirect(301, '/new-path')
})

app.post('/items', (req, res) => {
  res.status(201).json({ ok: true })
})

res.redirect defaults to 302; pass 301 explicitly for permanent moves or search engines keep hitting the old URL.

Shut down cleanly on SIGTERMgraceful-shutdown

const server = app.listen(3000)

process.on('SIGTERM', () => {
  server.close(() => {
    // close DB pools etc, then exit
    process.exit(0)
  })
})

app.listen returns a plain Node http.Server; without close() in-flight requests are cut off on deploys.

Alternatives

PackageRegistryPick it when
fastifynpmJSON API where throughput matters and you want schema-based validation built in
hononpmYou deploy to edge runtimes or want one codebase across Node, Bun, Deno, and Workers
koanpmYou want a smaller async-first core from the same lineage and will pick every middleware yourself