Building a Production-Ready REST API with Node.js, Express, and PostgreSQL
Building a Production-Ready REST API with Node.js, Express, and PostgreSQL
Introduction
REST APIs are the backbone of modern web and mobile applications. Whether you’re building a SaaS platform, a mobile app backend, or integrating third-party services, a well-designed API is critical.
In this tutorial, you’ll learn how to build a production-ready REST API using:
- Node.js – fast, event-driven runtime
- Express.js – lightweight web framework
- PostgreSQL – reliable relational database
- JWT Authentication – secure user access
- Best practices – structure, validation, error handling, and security
By the end, you’ll have a solid API foundation you can reuse for real-world projects.
What We’re Building
A simple User Management API with:
- User registration & login
- JWT-based authentication
- Protected routes
- PostgreSQL persistence
- Clean project structure
Endpoints overview:
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register | Create a new user |
| POST | /auth/login | Authenticate user |
| GET | /users/me | Get current user |
| GET | /health | API health check |
Prerequisites
You should be comfortable with:
- JavaScript fundamentals
- Basic HTTP concepts (GET, POST, status codes)
- Command-line usage
Required software:
- Node.js (v18+ recommended)
- PostgreSQL
- npm or yarn
Step 1: Project Setup
Create the project folder and initialize Node.js:
mkdir node-postgres-api
cd node-postgres-api
npm init -y
Install dependencies:
npm install express pg dotenv bcrypt jsonwebtoken express-validator
npm install --save-dev nodemon
Step 2: Project Structure
A clean structure improves maintainability:
src/
├── config/
│ └── db.js
├── controllers/
│ ├── auth.controller.js
│ └── user.controller.js
├── middlewares/
│ ├── auth.middleware.js
│ └── error.middleware.js
├── routes/
│ ├── auth.routes.js
│ └── user.routes.js
├── utils/
│ └── jwt.js
├── app.js
└── server.js
.env
Step 3: Environment Configuration
Create .env:
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=super_secure_secret
⚠️ Never commit
.envfiles to version control.
Step 4: Database Connection
src/config/db.js
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
module.exports = pool;
Step 5: Database Schema
Create a users table:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Step 6: Express App Initialization
src/app.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/health', (_, res) => {
res.json({ status: 'ok' });
});
module.exports = app;
src/server.js
require('dotenv').config();
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 7: Authentication Logic
JWT Utility
src/utils/jwt.js
const jwt = require('jsonwebtoken');
exports.signToken = (payload) =>
jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
Register Controller
src/controllers/auth.controller.js
const bcrypt = require('bcrypt');
const pool = require('../config/db');
const { signToken } = require('../utils/jwt');
exports.register = async (req, res) => {
const { email, password } = req.body;
const hashed = await bcrypt.hash(password, 10);
const user = await pool.query(
'INSERT INTO users(email, password) VALUES($1,$2) RETURNING id,email',
[email, hashed]
); const token = signToken({ id: user.rows[0].id }); res.status(201).json({ token }); };
Login Controller
exports.login = async (req, res) => {
const { email, password } = req.body;
const user = await pool.query(
'SELECT * FROM users WHERE email=$1',
[email]
); if (!user.rows.length) { return res.status(401).json({ error: ‘Invalid credentials’ }); } const valid = await bcrypt.compare(password, user.rows[0].password); if (!valid) { return res.status(401).json({ error: ‘Invalid credentials’ }); } const token = signToken({ id: user.rows[0].id }); res.json({ token }); };
Step 8: Authentication Middleware
src/middlewares/auth.middleware.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.sendStatus(401);
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.sendStatus(403);
}
};
Step 9: Protected User Route
src/controllers/user.controller.js
const pool = require('../config/db');
exports.me = async (req, res) => {
const user = await pool.query(
'SELECT id,email FROM users WHERE id=$1',
[req.user.id]
); res.json(user.rows[0]); };
Step 10: Routes
src/routes/auth.routes.js
const router = require('express').Router();
const ctrl = require('../controllers/auth.controller');
router.post('/register', ctrl.register);
router.post('/login', ctrl.login);
module.exports = router;
src/routes/user.routes.js
const router = require('express').Router();
const auth = require('../middlewares/auth.middleware');
const ctrl = require('../controllers/user.controller');
router.get('/me', auth, ctrl.me);
module.exports = router;
Mount routes in app.js:
app.use('/auth', require('./routes/auth.routes'));
app.use('/users', require('./routes/user.routes'));
Step 11: Error Handling Best Practice
Centralized error handling improves stability:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
Step 12: Security & Production Tips
✅ Use HTTPS
✅ Enable rate limiting
✅ Validate inputs (express-validator)
✅ Rotate JWT secrets
✅ Use migrations (e.g., Prisma / Knex)
✅ Log errors (Winston / Pino)
Conclusion
You’ve built a clean, secure, and scalable REST API with:
- JWT authentication
- PostgreSQL persistence
- Structured Express architecture
- Production-ready practices
This foundation can power SaaS platforms, dashboards, mobile apps, or automation systems.
Next Steps
- Add refresh tokens
- Implement role-based access
- Add tests (Jest + Supertest)
- Dockerize the API
- Add GraphQL layer (Apollo / WPGraphQL-style)
Related Tutorials
How to Fix “‘adb’ is not recognized as an internal or external command”
If you’re seeing this error when trying to use Android Debug Bridge (ADB), it means your system can’t find the ADB executable. This comprehensive guide will walk you through understanding the problem and multiple solutions to fix it. Understanding the Problem ADB (Android Debug Bridge) is a command-line tool that’s part of the Android SDK […]
How to Use Claude Code with AWS Bedrock
This guide shows how to run Claude Code using Anthropic models hosted on Amazon Bedrock, instead of Anthropic’s direct API. Overview Claude Code supports Amazon Bedrock as a backend. When enabled, it: Prerequisites 1. AWS Account with Bedrock Enabled 👉 AWS Bedrock Consolehttps://console.aws.amazon.com/bedrock/ Important: The first time you use Anthropic models, AWS requires you to […]
How to Enable ICMP (Ping) on Windows Public Firewall
This guide explains how to allow ICMP (Ping) traffic on a Windows machine using the Public firewall profile. This is useful for network troubleshooting, monitoring, and connectivity testing. ⚠️ Important: This only makes the Windows host pingable. It does not affect routers, public IPs on firewalls (e.g. MikroTik), or tunnel brokers like Hurricane Electric if […]