Express Basics
10 examples to build your first Express 5 API - 7 basic and 3 intermediate.
Prerequisites
mkdir express-api && cd express-api
npm init -y
npm pkg set type=module
npm install express@5
npm install -D typescript@5.6 tsx @types/express @types/nodeFor middleware ordering and security hardening, see Middleware Ordering and Security Middleware.
Basic Examples
1. Hello World Server
import express from "express";
const app = express();
const port = 3000;
app.get("/", (_req, res) => {
res.send("Hello from Express 5\n");
});
app.listen(port, () => {
console.log(`http://localhost:${port}`);
});appis a function that wraps Node'shttp.createServerres.send()setsContent-Typeautomatically based on the payload- Express 5 requires Node 18+; use Node 24 LTS in production
2. JSON Body Parsing
import express from "express";
const app = express();
app.use(express.json({ limit: "1mb" }));
app.post("/users", (req, res) => {
const { name, email } = req.body;
res.status(201).json({ id: 1, name, email });
});express.json()must be registered before routes that readreq.body- Set
limitto prevent large payload attacks - Fastify includes JSON parsing by default with schema validation
3. Route Parameters
import express from "express";
const app = express();
app.get("/users/:id", (req, res) => {
const { id } = req.params;
res.json({ id, name: "Ada Lovelace" });
});req.paramscontains named route segments- Express 5 changed wildcard routing syntax - see Express 5 Migration
- Validate
idformat in the handler or a validation middleware
4. Query Strings
import express from "express";
const app = express();
app.get("/search", (req, res) => {
const q = req.query.q as string | undefined;
const page = Number(req.query.page ?? 1);
res.json({ q, page, results: [] });
});req.queryis parsed automatically- Cast query values in TypeScript; they arrive as strings or arrays
- Use a validation library (Zod) at the boundary for production APIs
5. Router for Modular Routes
import express, { Router } from "express";
const app = express();
const usersRouter = Router();
usersRouter.get("/", (_req, res) => {
res.json([{ id: 1, name: "Ada" }]);
});
usersRouter.get("/:id", (req, res) => {
res.json({ id: req.params.id });
});
app.use("/users", usersRouter);Routeris a mini-app with its own middleware stack- Mount routers at path prefixes to organize large APIs
- NestJS modules serve a similar grouping purpose with DI
6. Middleware for Request Logging
import express from "express";
const app = express();
app.use((req, _res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
app.get("/health", (_req, res) => {
res.json({ status: "ok" });
});- Middleware runs in registration order
- Call
next()to pass control to the next handler - See Middleware Pattern for the underlying pattern
7. Static File Serving
import express from "express";
import { join } from "node:path";
const app = express();
app.use("/static", express.static(join(import.meta.dirname, "public")));express.staticserves files from a directory- Place static middleware before API routes or use a dedicated path prefix
- In production, nginx or a CDN usually serves static assets instead
Intermediate Examples
8. Async Route Handlers (Express 5)
import express from "express";
const app = express();
app.get("/users/:id", async (req, res) => {
const user = await findUser(req.params.id);
if (!user) {
res.status(404).json({ error: "Not found" });
return;
}
res.json(user);
});
async function findUser(id: string) {
if (id === "999") return null;
return { id, name: "Ada" };
}- Express 5 forwards rejected promises to error middleware automatically
- Express 4 required
express-async-errorsor manual try/catch wrappers - Always return after sending error responses to avoid double-send
9. Error Middleware
import express, { type Request, type Response, type NextFunction } from "express";
const app = express();
app.get("/fail", () => {
throw new Error("Something broke");
});
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err);
res.status(500).json({ error: "Internal server error" });
});- Error middleware has 4 parameters:
(err, req, res, next) - Register it after all routes
- Never expose stack traces to clients in production
10. Environment-Based Configuration
import express from "express";
const app = express();
const port = Number(process.env.PORT ?? 3000);
const isDev = process.env.NODE_ENV !== "production";
if (isDev) {
app.use((req, _res, next) => {
console.log(`[dev] ${req.method} ${req.path}`);
next();
});
}
app.get("/health", (_req, res) => {
res.json({ env: process.env.NODE_ENV ?? "development" });
});
app.listen(port);- Use
PORTfrom the environment (Heroku, Railway, K8s all set this) - Gate debug middleware behind
NODE_ENV - See Configuration Basics for schema validation
FAQs
Is Express 5 production-ready?
Yes on Node 24 LTS. It is the current npm latest major. Migrate from Express 4 using the official migration guide patterns in Express 5 Migration.
Express vs Fastify for a new project?
Express if team familiarity and ecosystem size matter most. Fastify if throughput and JSON schema validation are priorities. See Fastify vs Express ADR.
Should I use app.listen or http.createServer?
app.listen is fine for most apps. Use createServer(app) when you need direct access to the underlying server for WebSocket upgrades or timeout tuning.
How do I structure a large Express app?
Routers per domain (/users, /orders), services for business logic, separate app.ts and server.ts. See Express Best Practices.
Does Express support ESM?
Yes. Set "type": "module" in package.json and use import express from "express".
Related
- Middleware Ordering - registration sequence
- Error Handling in Express - async and sync errors
- Security Middleware - helmet, cors, rate limits
- Fastify Basics - performance alternative
- Express Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.