A Node.js API that works on a developer's laptop and a Node.js API that works reliably in production for thousands of daily users are not the same thing. The difference is not the core logic — it is the surrounding architecture, the error handling, the operational tooling, and the dozens of decisions that do not show up on a feature list but determine whether the system can be trusted with real users and real data.
This checklist reflects what I verify before a Node.js API can be considered production-ready. It is not a beginner's tutorial on how to write routes. It is a quality gate for APIs that need to run reliably without constant developer intervention.
Project Structure: Predictable, Modular, and Testable
The most common structure problem in Node.js APIs is everything living in one file or in a flat collection of route files with no separation of concerns. Route handlers that directly call database queries and send HTTP responses are untestable, inflexible, and become increasingly difficult to modify as the application grows.
A production Node.js API should follow a layered structure:
- Routes layer: Receives HTTP requests, validates input using a validation library (Zod or Joi), calls the appropriate service function, and sends the response. Contains no business logic.
- Service layer: Contains business logic. Does not know about HTTP — it receives validated data, performs operations, and returns results or throws domain-specific errors. This layer is unit-testable without HTTP setup.
- Repository/Data layer: Handles all database interactions. Returns typed objects. Does not know about HTTP or business rules.
- Middleware layer: Authentication verification, rate limiting, request logging, error handling. Applied to routes globally or selectively.
This separation means a service function can be tested with a mock repository without spinning up an HTTP server, and route handlers can be tested with mock service functions without touching a database. Testability and maintainability are the same thing.
Input Validation: Never Trust the Request
Every piece of data that enters the API from an HTTP request — URL parameters, query strings, request bodies, headers — must be validated before it reaches business logic. Node.js does not enforce types at runtime. A route that expects a numeric user ID in the URL will receive a string. A route that expects an email address in the body will receive whatever the caller sends.
Zod is the most ergonomic validation library for modern Node.js APIs. Define a schema for each request shape and parse incoming data against it. If parsing fails, Zod returns a structured error that can be forwarded directly to the API consumer as a 422 validation error. If parsing succeeds, the validated data is correctly typed and safe to pass to the service layer.
The critical rule: validated data flows forward; raw request data does not. A route handler should never pass req.body directly to a database operation or a service function.
Error Handling: Centralized, Not Scattered
Error handling in Express (and most Node.js frameworks) is provided through error-handling middleware — a function with four parameters (err, req, res, next) registered at the end of the middleware stack. Every unhandled error in any route or middleware is forwarded to this function.
A production error handler must:
- Distinguish between operational errors (validation failures, not-found errors, business logic rejections) and programmer errors (unexpected exceptions, null reference errors, database connection failures)
- Return appropriate HTTP status codes for each error type
- Return a clean JSON error structure, never an HTML page or a stack trace
- Log unexpected errors to a monitoring service (never swallow them silently)
- In production, return only a safe message to the client; log the full error internally
Custom error classes for operational errors make this categorization clean. An AppError class that extends Error with a statusCode and isOperational flag allows the error handler to check whether an error was deliberately thrown (operational) or unexpected (programmer error) and respond accordingly.
Authentication and Authorisation: Verified Every Request
Every endpoint that is not intentionally public must verify authentication on every request. In Node.js APIs, this is done through middleware that reads the Authorization header, validates the JWT or token, and attaches the verified user identity to the request object.
JWT verification must check the token signature (using the correct secret), the expiry claim, and any audience or issuer claims relevant to the application. An expired token should return 401, not be silently allowed through.
Authorisation — verifying what the authenticated user is permitted to do — must be checked at the route or service level, not just at the authentication level. A user who is authenticated can only perform actions their role permits. This check belongs in the service layer where the business rule is enforced, not only in the route handler where it can be bypassed by calling the service function directly.
Database Connections: Pool Management and Graceful Shutdown
Node.js APIs must manage database connection pools carefully. Creating a new database connection for every request is extremely slow and will exhaust available connections under any meaningful load. Use a connection pool (pg for PostgreSQL, mysql2 for MySQL, Mongoose connection pool for MongoDB) configured with appropriate minimum and maximum connection counts for the expected concurrency.
Graceful shutdown is a frequently skipped requirement. When a Node.js process receives a SIGTERM signal (from a deployment, a restart, or a crash recovery), it should stop accepting new requests, wait for in-flight requests to complete, close all open database connections, and then exit cleanly. Without graceful shutdown, in-flight database transactions may be interrupted, causing data inconsistencies.
Environment Configuration: Validated at Startup
All environment-specific configuration — database connection strings, JWT secrets, third-party API keys, service URLs — must come from environment variables. The application must never hardcode these values or fall back to insecure defaults.
Validate all required environment variables at application startup using Zod or a similar tool. If a required variable is missing, the application should refuse to start and log a clear error identifying the missing variable. An application that starts without a required secret and silently fails when that secret is needed in production is far harder to debug than one that refuses to start at all.
Rate Limiting and Request Size Limits
Without rate limiting, your API is vulnerable to abuse by high-volume clients, whether malicious scrapers or accidentally misconfigured services. The express-rate-limit middleware applies per-IP or per-user rate limits globally or to specific route groups. Authenticated endpoints should use per-user limits rather than per-IP limits to avoid penalising users behind shared NAT addresses.
Without request size limits, a client can send a request body of arbitrary size that exhausts server memory. Express's built-in body parsers accept a size limit option. Set a conservative default (1MB or less for JSON APIs) and increase it specifically for endpoints that legitimately receive large payloads.
If you are building a Node.js backend API and want a production-ready architecture from the start, visit my Node.js development services page to learn how I approach backend API delivery for production systems.