Designing a scalable API in Node.js means creating a backend architecture that is modular, secure, performant, and easy to maintain as your application grows. In this guide, weβll cover best practices and examples of building scalable REST APIs using Express.js, MongoDB (with Mongoose), and supporting tools.
A clean and scalable folder structure separates concerns and enables team collaboration.
project-root/
βββ config/
β βββ db.js
βββ controllers/
β βββ user.controller.js
βββ middlewares/
β βββ auth.js
βββ models/
β βββ user.model.js
βββ routes/
β βββ v1/
β βββ user.routes.js
βββ services/
β βββ user.service.js
βββ utils/
β βββ logger.js
βββ validations/
β βββ user.validation.js
βββ app.js
βββ server.js
βββ .env
Each route is encapsulated in its own module.
const express = require('express');
const router = express.Router();
const userController = require('../../controllers/user.controller');
router.get('/', userController.getAllUsers);
router.post('/', userController.createUser);
module.exports = router;
const userService = require('../services/user.service');
exports.getAllUsers = async (req, res) => {
const users = await userService.getAll();
res.json(users);
};
exports.createUser = async (req, res) => {
const user = await userService.create(req.body);
res.status(201).json(user);
};
Versioning ensures backward compatibility and smooth transition across updates.
const express = require('express');
const app = express();
const userRoutesV1 = require('./routes/v1/user.routes');
app.use('/api/v1/users', userRoutesV1);
npm install dotenv
PORT=5000
DB_URI=mongodb://localhost:27017/scalable_api
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected');
} catch (err) {
console.error('DB Connection Error:', err.message);
process.exit(1);
}
};
module.exports = connectDB;
npm install joi
const Joi = require('joi');
exports.createUser = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required()
});
const errorHandler = (err, req, res, next) => {
console.error(err);
res.status(500).json({ error: err.message });
};
module.exports = errorHandler;
exports.getAllUsers = async (req, res) => {
const { page = 1, limit = 10 } = req.query;
const users = await userService.getAllPaginated(page, limit);
res.json(users);
};
const User = require('../models/user.model');
exports.getAllPaginated = async (page, limit) => {
const skip = (page - 1) * limit;
return await User.find().skip(skip).limit(Number(limit));
};
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100
});
app.use(limiter);
npm install morgan
const morgan = require('morgan');
app.use(morgan('combined'));
npm install helmet
const helmet = require('helmet');
app.use(helmet());
npm install --save-dev jest supertest
const request = require('supertest');
const app = require('../app');
describe('GET /api/v1/users', () => {
it('should return users', async () => {
const res = await request(app).get('/api/v1/users');
expect(res.statusCode).toEqual(200);
});
});
npm install swagger-jsdoc swagger-ui-express
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Scalable API',
version: '1.0.0'
},
},
apis: ['./routes/**/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = { swaggerUi, specs };
const { swaggerUi, specs } = require('./docs/swagger');
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
Scalable API design requires careful planning of structure, modularity, and tools. Following the principles of separation of concerns, validation, documentation, and performance optimization ensures that your Node.js API remains robust and easy to maintain as it grows. The combination of Express, Mongoose, and supporting libraries gives you the flexibility to develop fast while staying enterprise-grade.
A function passed as an argument and executed later.
Runs multiple instances to utilize multi-core systems.
Reusable blocks of code, exported and imported using require() or import.
nextTick() executes before setImmediate() in the event loop.
Starts a server and listens on specified port.
Node Package Manager β installs, manages, and shares JavaScript packages.
A minimal and flexible web application framework for Node.js.
A stream handles reading or writing data continuously.
It processes asynchronous callbacks and non-blocking I/O operations efficiently.
Node.js is a JavaScript runtime built on Chrome's V8 engine for server-side scripting.
An object representing the eventual completion or failure of an asynchronous operation.
require is CommonJS; import is ES6 syntax (requires transpilation or newer versions).
Use module.exports or exports.functionName.
Variables stored outside the code for configuration, accessed using process.env.
MongoDB, often used with Mongoose for schema management.
Describes project details and manages dependencies and scripts.
Synchronous blocks execution; asynchronous runs in background without blocking.
Allows or restricts resources shared between different origins.
Use try-catch, error events, or middleware for error handling.
Provides file system-related operations like read, write, delete.
Using event-driven architecture and non-blocking I/O.
Functions in Express that execute during request-response cycle.
A set of routes or endpoints to interact with server logic or databases.
Yes, it's single-threaded but handles concurrency using the event loop and asynchronous callbacks.
Middleware to parse incoming request bodies, like JSON or form data.
Copyrights © 2024 letsupdateskills All rights reserved