Building a Scalable REST API with Node.js, TypeScript, and PostgreSQL
A production architecture guide for Node.js REST APIs — from project structure and validation to connection pooling, error handling, and the patterns that keep APIs maintainable at scale.
By POINTNEXIS Team

A Node.js REST API that works in development and one that scales to production are two different things. Connection management, error propagation, input validation, and observability separate robust backends from fragile ones.
This guide covers the architecture decisions that matter for APIs handling real traffic across a growing user base.
Project Structure and Layered Architecture
Organize by layer, not by feature type. A structure with `routes/`, `controllers/`, `services/`, and `repositories/` directories keeps concerns separated. Routes handle HTTP mapping, controllers validate input and marshal responses, services contain business logic, and repositories handle database queries.
Use TypeScript path aliases (`@/services/user`) over relative imports (`../../services/user`). With strict TypeScript, the compiler catches layer violations — a service should never import from a route file.
Input Validation with Zod
Validate all incoming request bodies, query parameters, and path parameters at the route boundary before any business logic runs. Zod is the standard for TypeScript-first validation — schemas serve as both runtime validators and type definitions.
Return structured validation errors in a consistent format: `{ errors: [{ field: 'email', message: 'Invalid email format' }] }`. Clients need machine-readable errors to surface helpful messages in UI forms. Zod's `.flatten()` method converts nested errors into this format.
PostgreSQL Connection Pooling
Never create a new database connection per request — connection setup is expensive and PostgreSQL has connection limits. Use `pg` with a `Pool` instance configured with `max: 20` connections, `idleTimeoutMillis: 30000`, and `connectionTimeoutMillis: 2000`.
For serverless deployments (AWS Lambda, Vercel Functions), use PgBouncer or Supabase's connection pooler in transaction mode — connection pooling at the application level is not sufficient when functions spin up and down rapidly, creating hundreds of simultaneous connections.
Error Handling and Observability
Implement a global error handler that catches unhandled errors, logs them with structured context (request ID, user ID, route, error stack), and returns a consistent error response shape. Never expose internal error messages or stack traces to API consumers.
Instrument every request with a unique request ID propagated through all log lines. Use a structured logger (Pino) rather than `console.log`. Ship logs to a centralized platform (CloudWatch, Datadog, Logtail) and set up alerts on error rate spikes.