Back to Blog
SaaS & Web9 min read

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

MacBook laptop flat lay on a white desk showing backend code

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.