How to Build a REST API With Node.js, Express, and PostgreSQL
A complete guide to building production-ready REST APIs with Node.js, Express, and PostgreSQL — from project setup to authentication, validation, error handling, and deployment.
DL
Shantanu Kumar
Chief Solutions Architect
March 12, 2026
18 min read
Updated March 2026
Building a REST API is one of the most common tasks in modern software development. Whether you are building a mobile app backend, a SaaS dashboard, or an internal tool, the API layer is the foundation everything else depends on. Get it wrong and every feature built on top becomes harder to ship, harder to debug, and harder to scale.
This guide walks through building a production-grade REST API using Node.js with Express and PostgreSQL. We are not building a toy. We are building the same architecture pattern our team uses for client projects — with proper error handling, input validation, authentication, database migrations, and a structure that scales from one endpoint to hundreds.
A production API is not just endpoints that return JSON. It is a system with predictable error behavior, validated inputs, authenticated access, and observable performance.
1) Project structure and initial setup
Start with a clear folder structure. Mixing routes, business logic, and database queries into the same files is the fastest way to create unmaintainable code. We separate concerns from day one: routes define URL patterns, controllers handle request/response logic, services contain business rules, and models interact with the database.
bashProject Structure
1project/
2├──src/
3│├──config/
4││└──database.js#PostgreSQLconnectionpool
5│├──middleware/
6││├──auth.js#JWTauthentication
7││├──validate.js#Requestvalidation
8││└──errorHandler.js#Globalerrorhandler
9│├──routes/
10││├──index.js#Routeaggregator
11││├──users.js#Userendpoints
12││└──projects.js#Projectendpoints
13│├──controllers/
14││├──userController.js
15││└──projectController.js
16│├──services/
17││├──userService.js
18││└──projectService.js
19│├──models/
20││├──userModel.js
21││└──projectModel.js
22│└──app.js#Expressapplicationsetup
23├──migrations/
24│└──001_initial_schema.sql
25├──package.json
26└──server.js#Entrypoint
Initialize the project and install the core dependencies. We use pg for PostgreSQL, express for HTTP routing, jsonwebtoken for auth tokens, bcrypt for password hashing, and joi for input validation.
Never create a new database connection per request. Connection pooling reuses a fixed number of connections, which eliminates connection overhead and prevents exhausting your database under load. The pg library provides a built-in Pool class that handles this automatically.
Notice the slow query warning. In production, you want visibility into database performance from day one. A 200ms threshold catches most problem queries before they cause user-facing latency. This single pattern has saved us hours of debugging on client projects.
3) Database schema and migrations
Define your schema in SQL migration files, not in application code. Migration files are version-controlled, reviewable, and reproducible across environments. Every team member and every deployment gets the exact same database structure.
UUIDs as primary keys prevent enumeration attacks and make multi-database merges easier. The CHECK constraints on status and role enforce data integrity at the database level, which is stronger than application-level validation alone. The trigger function automatically maintains updated_at timestamps without requiring application code to remember to set them.
4) Express application setup with security middleware
Configure Express with production security defaults from the start. Helmet sets secure HTTP headers, CORS controls cross-origin access, and a JSON body parser with size limits prevents payload abuse. Adding these after the fact is how security vulnerabilities ship to production.
Never trust client input. Every request body, query parameter, and URL parameter should be validated before it reaches your business logic. Joi provides schema-based validation that is expressive, composable, and produces clear error messages. We create a reusable middleware that wraps any Joi schema.
The stripUnknown option silently removes fields that are not in the schema, preventing unexpected data from reaching your database. Setting abortEarly to false collects all validation errors at once instead of returning on the first failure, which gives API consumers a much better developer experience.
6) JWT authentication middleware
Authentication middleware verifies the JWT token on every protected request and attaches the decoded user payload to the request object. This keeps auth logic in one place instead of duplicated across controllers.
The authorize function is a higher-order middleware that checks user roles. You can chain it after authenticate to protect admin-only endpoints. This pattern — authenticate then authorize — is how enterprise APIs enforce least-privilege access without cluttering controller logic.
7) Model layer — clean database queries
Models encapsulate all SQL queries. Controllers never write raw SQL. This separation means you can swap databases, optimize queries, or add caching without touching your route logic. Each model function takes parameters and returns plain JavaScript objects.
Controllers are thin. They extract data from the request, call service or model functions, and format the response. No business logic lives here. This makes controllers easy to test and easy to read during code review.
Routes wire HTTP methods and URL patterns to controller functions. Middleware like authentication and validation are applied at the route level, making it immediately clear which endpoints are protected and what input they expect.
A centralized error handler catches every unhandled error in the request pipeline. It logs the full error for debugging while returning a safe, consistent response to the client. In production, never expose stack traces or internal error details to API consumers.
Catching PostgreSQL-specific error codes like 23505 (unique violation) and 23503 (foreign key violation) lets you return meaningful HTTP status codes instead of generic 500 errors. Your API consumers will thank you when they can distinguish between a duplicate email and a server crash.
11) Server entry point and graceful shutdown
The server entry point starts the HTTP listener and handles graceful shutdown. When the process receives a termination signal, it stops accepting new connections, waits for in-flight requests to complete, closes the database pool, and exits cleanly. This prevents dropped requests during deployments.
Before writing automated tests, verify each endpoint manually. Curl commands are fast, repeatable, and easy to share with teammates. Here is the complete flow from registration through CRUD operations on projects.
Before deploying, add rate limiting to prevent abuse. A simple in-memory rate limiter works for single-server deployments. For multi-server setups, use Redis-backed rate limiting. Also add request ID tracking for correlating logs across services.
Parameterized queries everywhere — never concatenate user input into SQL strings. The pg library handles parameterization with $1, $2 placeholders.
Passwords are hashed with bcrypt at 12 salt rounds. Never store plaintext passwords, and never use fast hashing algorithms like MD5 or SHA for passwords.
JWT tokens carry minimal claims (id, email, role). Do not put sensitive data in tokens because they are base64-encoded, not encrypted.
Database constraints (UNIQUE, CHECK, FOREIGN KEY) enforce data integrity even if application code has bugs.
The error handler catches PostgreSQL error codes and returns appropriate HTTP status codes instead of generic 500s.
Connection pooling with pg.Pool prevents connection exhaustion under concurrent load.
Graceful shutdown ensures zero dropped requests during deployments.
Common mistakes to avoid
After building dozens of production APIs for clients, these are the mistakes we see most often. Each one seems minor in development but causes real problems at scale.
Returning password hashes in API responses — always destructure or exclude password fields before sending user data.
Missing pagination — returning all rows works with 10 records but crashes with 10,000.
No input size limits — without body parser limits, a single request can exhaust server memory.
Catching errors silently — swallowing errors without logging makes debugging in production nearly impossible.
Hardcoding configuration — database credentials, JWT secrets, and API keys belong in environment variables, never in source code.
Skipping database indexes — every column used in WHERE clauses or JOIN conditions needs an index. The performance difference is orders of magnitude.
Next steps
This guide covers the foundation. From here, consider adding automated tests with a test database, request logging with structured JSON output, API documentation with OpenAPI/Swagger, file upload handling with multer, WebSocket support for real-time features, and containerization with Docker for consistent deployments.
The architecture pattern in this guide is the same one we use at Dude Lemon for client API projects. It scales from a weekend prototype to a production system serving thousands of users. If you are building an API for your business and want experienced engineers to review your architecture or handle the build, reach out to our team for a free consultation.
A well-structured API is not over-engineered — it is an investment that pays for itself the first time you need to debug a production issue at 2 AM.
Need help building this?
Let our team build it for you.
Dude Lemon builds production-grade web apps, APIs, and cloud infrastructure. Get a free consultation and project proposal within 48 hours.