Node.js REST API with Express: Complete Guide 2026
Express in 2026: Still the Go-To for Node.js APIs
Express remains the most widely used Node.js web framework in 2026 despite the emergence of alternatives (Fastify, Hono, Elysia). Its simplicity, ecosystem breadth, and the enormous body of existing knowledge and tooling make it the lowest-friction choice for building REST APIs. Fastify is the correct choice when raw performance is the primary concern — it's significantly faster than Express. But for the majority of API projects where developer productivity and ecosystem access matter more than nanosecond-level throughput, Express provides the best starting point.
Project Structure for Production APIs
Organize Express applications by feature rather than by type. Instead of routes/, controllers/, models/ at the top level, use features/users/, features/products/, features/orders/ — each containing the route definition, controller logic, validation, and types for that feature. This structure scales better than the traditional MVC layout because all code related to a feature lives together. Keep middleware in a shared middleware/ directory and utilities in lib/.
Authentication with JWT
Implement JWT authentication as Express middleware. The middleware extracts the Authorization header, verifies the token signature with jsonwebtoken, and attaches the decoded user payload to req.user. Apply the middleware to protected routes using router.use(authMiddleware) or individually. Store JWT secrets in environment variables, use RS256 for APIs that need multiple verification endpoints, and implement refresh tokens with short-lived access tokens (15 minutes) and longer-lived refresh tokens (7 days) stored in httpOnly cookies.
Request Validation with Zod
Never trust incoming request data. Validate all request bodies, query parameters, and URL parameters against a schema before processing. Zod provides TypeScript-native schema validation with excellent error messages. Define a schema for each request type, run validation in middleware before the controller, and return 422 with field-level error details when validation fails. Zod schemas double as TypeScript types — parse the request body with z.parse() and the result is fully typed throughout the controller function.
Error Handling
Express error handling uses four-argument middleware: (err, req, res, next). Register the error handler after all routes. Create a custom AppError class with statusCode and isOperational properties to distinguish operational errors (invalid input, not found) from programming errors (null pointer, database connection failure). The error handler returns structured JSON error responses for operational errors and logs programming errors without exposing details to the client. Use express-async-errors or wrap all async route handlers in a try-catch utility to ensure async errors reach the error handler. Download our Node.js Express API starter template at proofmatcher.com.
Express 5: What Changed
Express 5 is now the current major version, and it fixes one of Express 4's biggest pain points. Route handlers and middleware can be async functions, and if they throw or return a rejected promise, Express 5 automatically passes the error to your error-handling middleware. In Express 4 an unhandled rejection in a route could hang the request or crash the process unless you wrapped every handler. Express 5 also updates its path-matching syntax, so some wildcard and optional-parameter routes need small changes, and removes a few long-deprecated methods. Check the official migration guide before upgrading an existing API.
Security Middleware and Configuration
- Helmet sets sensible security headers, such as a Content Security Policy and protection against clickjacking, with one line of middleware.
- CORS should list the exact origins that may call the API rather than allowing every origin, especially when the API uses cookies.
- Rate limiting with a package such as
express-rate-limitprotects login and other sensitive endpoints from brute-force attacks. Use a shared store such as Redis when you run several instances. - Body size limits on
express.json({ limit: "100kb" })stop oversized payloads from exhausting memory. - Trust proxy must be configured when the API runs behind Nginx or a load balancer, so Express reads the real client IP for logging and rate limiting.
Load configuration from environment variables, validate it at startup, and fail immediately if a required value such as the database URL or JWT secret is missing.
Structured Logging and Health Checks
Replace console.log with a structured logger such as Pino, which writes JSON lines that log platforms can search and filter. Log one line per request with the method, path, status code, duration, and a request ID, and include the same request ID in error logs so you can trace a failure end to end. Never log passwords, tokens, or full payment details. Add a lightweight /health endpoint that returns 200 when the process is running, and optionally a readiness check that confirms the database connection, so load balancers and container platforms know when to send traffic.
Graceful Shutdown
When you deploy a new version, the old process receives a termination signal. Without handling it, in-flight requests are cut off. Listen for SIGTERM, stop accepting new connections with server.close(), let existing requests finish, then close database pools and exit. Add a timeout so a stuck request cannot block shutdown forever. This small addition makes deployments invisible to users.
Testing Your API
Export the Express app separately from the code that calls listen(), so tests can import it without opening a port. Supertest sends real HTTP requests to the app in memory and lets you assert on status codes, headers, and response bodies. Write tests for the happy path, validation failures, missing authentication, and access to other users' resources, and run them against a dedicated test database that is reset between runs. With validation, error handling, security middleware, logging, and tests in place, an Express API is ready for production traffic.
Performance Tips
Most Express APIs are limited by the database, not by Express itself. Add indexes for the queries your endpoints run, use a connection pool, paginate list endpoints, and cache responses that are expensive to compute. Enable compression at the reverse proxy, run one Node.js process per CPU core with a process manager or container replicas, and move slow work such as sending emails or generating reports to a background job queue so requests return quickly.
Documenting the API with OpenAPI
An API without documentation is hard to use and easy to break. Describe your endpoints in an OpenAPI specification, either written by hand or generated from your validation schemas; libraries exist that convert Zod schemas into OpenAPI definitions, so validation and documentation never drift apart. Serve interactive documentation with a tool such as Swagger UI or Scalar on a protected route, and use the specification to generate typed clients for your frontend.
Pagination and Versioning in Practice
Every list endpoint should be paginated from the start, because adding it later breaks clients. Accept limit with a sensible maximum, and prefer cursor-based pagination for large or frequently changing collections. Return the items along with a nextCursor value that the client sends back to fetch the next page. For versioning, mount routers under a prefix such as app.use("/v1", v1Router), so a future /v2 can live alongside the old version during a migration period.
When to Consider Fastify or Hono
Express is a safe default, but alternatives have real strengths. Fastify offers higher throughput, built-in schema validation, and a structured plugin system, which suits high-traffic APIs. Hono is small and runs on many JavaScript runtimes, including Cloudflare Workers, Deno, and Bun, which suits edge deployments. Choose based on where the API will run and what your team knows; the architectural practices in this guide apply equally to all three.