All Articles
Tutorial10 min read

Building Production-Ready REST APIs with Node.js and Express

Learn how to build scalable REST APIs that can handle real traffic. Covers authentication, validation, error handling, and deployment.

T

TechGyanic

December 17, 2025

Building Production-Ready REST APIs with Node.js

I remember deploying my first Node.js API to a VPS I rented for ₹500/month. It crashed within hours because I hadn't considered what "production-ready" actually meant. Here's everything I learned the hard way.

Project Structure That Scales

Forget the tutorials that put everything in app.js. Here's how real companies organize their Node projects:

src/
├── config/
│   ├── database.js
│   └── environment.js
├── controllers/
├── middleware/
├── models/
├── routes/
├── services/
├── utils/
└── app.js

Setting Up Express Properly

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');

const app = express();

// Security middleware
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));

// Rate limiting - essential for production
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per window
});
app.use('/api/', limiter);

app.use(express.json({ limit: '10kb' }));

Authentication Done Right

JWT is industry standard, but implementation matters:

const jwt = require('jsonwebtoken');

const generateTokens = (userId) => {
  const accessToken = jwt.sign(
    { userId },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );
  
  const refreshToken = jwt.sign(
    { userId },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );
  
  return { accessToken, refreshToken };
};

Store refresh tokens in Redis or your database, not just in memory.

Input Validation

Never trust user input. I use Joi for validation:

const Joi = require('joi');

const userSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).pattern(/^(?=.*[A-Z])(?=.*\d)/).required(),
  phone: Joi.string().pattern(/^[6-9]\d{9}$/) // Indian mobile format
});

const validateUser = (req, res, next) => {
  const { error } = userSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  next();
};

Error Handling

Create a centralized error handler:

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

const errorHandler = (err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  
  res.status(statusCode).json({
    status: 'error',
    message: err.isOperational ? err.message : 'Something went wrong'
  });
  
  if (!err.isOperational) {
    console.error('CRITICAL ERROR:', err);
  }
};

Database Connection with Retry Logic

const mongoose = require('mongoose');

const connectDB = async (retries = 5) => {
  try {
    await mongoose.connect(process.env.MONGODB_URI);
    console.log('Database connected');
  } catch (err) {
    if (retries > 0) {
      console.log(`Retrying connection... (${retries} attempts left)`);
      await new Promise(r => setTimeout(r, 5000));
      return connectDB(retries - 1);
    }
    throw err;
  }
};

Deployment Checklist

Before going live:

  • Environment variables properly set
  • CORS configured for production domains
  • Rate limiting enabled
  • Logging configured (use Winston or Pino)
  • Health check endpoint at /health
  • Process manager (PM2) configured

Your API should handle failures gracefully. Users don't care why something broke—they just want it to work.

nodejsexpressapibackendjavascript
Share this article
T

Written by

TechGyanic

Sharing insights on technology, software architecture, and development best practices.