LINESERVE
All tutorials
Web DevelopmentAll levels

Building a Production-Ready REST API with Node.js, Express, and PostgreSQL

Build a production-ready REST API with Node.js, Express, and PostgreSQL, adding JWT auth, bcrypt password hashing, validation, and a clean project structure.

Stephen NdegwaPublished Last updated 3 min read
Share
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:

MethodEndpointDescription
POST/auth/registerCreate a new user
POST/auth/loginAuthenticate user
GET/users/meGet current user
GET/healthAPI 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 .env files 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)