Node Js

How to make scalable Node JS API – Complete guide

This in a practical architecture style: how to structure the API, handle high traffic, scale database, use caching, queues, load balancing, Docker/PM2, monitoring, and deployment patterns.

Scalable Node.js API — Complete Guide

A scalable API means:

More users
More requests
More data
Still fast and stable

1. Use proper folder structure

src/
├── config/
│ └── db.js
├── controllers/
│ └── userController.js
├── routes/
│ └── userRoutes.js
├── services/
│ └── userService.js
├── models/
│ └── User.js
├── middlewares/
│ ├── auth.js
│ ├── errorHandler.js
│ └── validate.js
├── utils/
│ └── logger.js
└── server.js

Use this pattern:

Route → Controller → Service → Model

Do not write all logic inside route files.


2. Use environment variables

PORT=5000
MONGO_URI=mongodb://127.0.0.1:27017/myapp
JWT_SECRET=strong_secret_key
REDIS_URL=redis://localhost:6379
NODE_ENV=production

Do not hardcode database URL, password, API keys, or tokens.


3. Use async error handling

const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};

module.exports = asyncHandler;

Usage:

exports.getUsers = asyncHandler(async (req, res) => {
const users = await User.find();

res.json({
success: true,
data: users
});
});

4. Use global error handler

app.use((err, req, res, next) => {
console.error(err);

res.status(err.statusCode || 500).json({
success: false,
message: err.message || "Server error"
});
});

5. Use database indexing

Without index:

User.find({ email: "test@gmail.com" });

MongoDB may scan many records.

With index:

const userSchema = new mongoose.Schema({
name: String,
email: {
type: String,
index: true,
unique: true
},
city: {
type: String,
index: true
}
});

For job portal:

jobSchema.index({ title: "text", skills: "text", city: "text" });
jobSchema.index({ city: 1, experience: 1 });
jobSchema.index({ slug: 1 });

6. Use pagination

Never return all records.

Bad:

const users = await User.find();

Good:

const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 10;
const skip = (page - 1) * limit;

const users = await User.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });

res.json({
page,
limit,
data: users
});

For large data, cursor pagination is better:

const users = await User.find({
_id: { $gt: req.query.lastId }
})
.limit(10)
.sort({ _id: 1 });

7. Use Redis caching

Use Redis for frequently requested data like:

Job categories
City list
Homepage jobs
Popular jobs
User session
API response cache

Redis provides an official Node.js client called node-redis.

Install:

npm install redis
const redis = require("redis");

const client = redis.createClient({
url: process.env.REDIS_URL
});

client.connect();

module.exports = client;

Example cache middleware:

const redisClient = require("../config/redis");

const cache = (keyPrefix, expireTime = 60) => {
return async (req, res, next) => {
const key = `${keyPrefix}:${req.originalUrl}`;

const cachedData = await redisClient.get(key);

if (cachedData) {
return res.json(JSON.parse(cachedData));
}

res.sendResponse = res.json;

res.json = async (body) => {
await redisClient.setEx(key, expireTime, JSON.stringify(body));
res.sendResponse(body);
};

next();
};
};

module.exports = cache;

Usage:

router.get("/jobs", cache("jobs", 60), getJobs);

8. Use clustering / PM2

Node.js runs JavaScript on a single main thread. To use multiple CPU cores, use clustering. Node’s cluster module can run multiple Node.js processes that share the same server port.

PM2 method

npm install pm2 -g
pm2 start server.js -i max
pm2 save
pm2 startup

Meaning:

-i max = use all CPU cores

Check status:

pm2 status
pm2 logs
pm2 monit

9. Use worker threads for CPU-heavy tasks

Use worker threads for:

Resume parsing
PDF processing
Image processing
Large Excel import
AI scoring
Heavy calculations

Node.js docs say worker threads are useful for CPU-intensive JavaScript operations, but not very useful for I/O-heavy work.

Example:

const { Worker } = require("worker_threads");

function runWorker(filePath) {
return new Promise((resolve, reject) => {
const worker = new Worker("./workers/resumeWorker.js", {
workerData: filePath
});

worker.on("message", resolve);
worker.on("error", reject);
});
}

Do not process heavy files directly inside API request.


10. Use queue system for background jobs

For long tasks, use queue:

User uploads resume

API stores file

Add job to queue

Worker processes resume

Save result in database

Use:

npm install bullmq ioredis

Example use cases:

Send emails
Parse resumes
Generate reports
Import Excel
Webhook retry
Candidate matching
Notification sending

11. Use load balancer

When one server is not enough:

Users

Nginx / Load Balancer

Node Server 1
Node Server 2
Node Server 3

MongoDB / Redis

Example Nginx:

upstream node_api {
server 127.0.0.1:5000;
server 127.0.0.1:5001;
server 127.0.0.1:5002;
}

server {
listen 80;

location / {
proxy_pass http://node_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

12. Use compression

Express production performance guidance recommends gzip compression for response bodies.

npm install compression
const compression = require("compression");

app.use(compression());

13. Avoid blocking code

Bad:

const fs = require("fs");

const data = fs.readFileSync("large-file.txt");

Good:

const fs = require("fs/promises");

const data = await fs.readFile("large-file.txt");

Avoid:

Large loops
Synchronous file read
Heavy PDF parsing in request
Large JSON response
Unindexed database queries

14. Use connection pooling

MongoDB/Mongoose manages pooling, but configure properly:

mongoose.connect(process.env.MONGO_URI, {
maxPoolSize: 20,
serverSelectionTimeoutMS: 5000
});

15. Use API versioning

/api/v1/users
/api/v1/jobs
/api/v2/jobs

Example:

app.use("/api/v1/users", userRoutes);

This helps when you update your API without breaking old apps.


16. Use logging and monitoring

Use logs for:

API errors
Slow requests
Login failures
Payment issues
Database failures

Install:

npm install winston morgan
const morgan = require("morgan");

app.use(morgan("combined"));

In production, monitor:

CPU usage
Memory usage
API response time
Error rate
Database query time
Redis usage
Server disk space

17. Use health check API

app.get("/health", (req, res) => {
res.json({
success: true,
message: "API running",
uptime: process.uptime(),
timestamp: new Date()
});
});

Load balancer can check this route.


18. Use stateless API

Bad scalable design:

Store user session in Node memory

Good scalable design:

JWT token
Redis session
Database session

Because multiple servers should handle the same user.


19. Use CDN for static files

Do not serve images, resumes, PDFs directly from Node for high traffic.

Use:

S3 / Cloud Storage / CDN

Flow:

User uploads resume

Store file in S3

Save file URL in MongoDB

20. Recommended scalable architecture

For your recruitment/job portal:

Frontend
Next.js / React

Nginx / Load Balancer

Node.js API Servers

Redis Cache

MongoDB / MySQL

Queue Workers

File Storage / CDN

Complete scalable server.js example

const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const compression = require("compression");
const rateLimit = require("express-rate-limit");
const mongoose = require("mongoose");
require("dotenv").config();

const userRoutes = require("./routes/userRoutes");

const app = express();

app.use(helmet());
app.use(compression());

app.use(cors({
origin: [
"https://cybotrix.com",
"https://admin.cybotrix.com",
"http://localhost:5173"
],
credentials: true
}));

app.use(express.json({ limit: "1mb" }));

const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 300
});

app.use("/api", limiter);

app.get("/health", (req, res) => {
res.json({
success: true,
uptime: process.uptime()
});
});

app.use("/api/v1/users", userRoutes);

app.use((err, req, res, next) => {
console.error(err);

res.status(500).json({
success: false,
message: "Server error"
});
});

mongoose
.connect(process.env.MONGO_URI, {
maxPoolSize: 20
})
.then(() => {
app.listen(process.env.PORT || 5000, () => {
console.log("Server running");
});
});

Final checklist

Use proper folder structure
Use pagination
Use database indexes
Use Redis caching
Use PM2 cluster mode
Use queue for background jobs
Use worker threads for CPU-heavy tasks
Use load balancer
Use compression
Avoid blocking code
Use API versioning
Use centralized logging
Use health check endpoint
Use CDN for files
Keep API stateless
Scale database separately

For a job portal API, your best first scalable setup is:

Node.js + Express
MongoDB/MySQL with indexes
Redis cache
PM2 cluster
Nginx reverse proxy
Queue for resume parsing
S3/CDN for resume files
Monitoring + logs