Web Development

Building Scalable APIs with Node.js: Best Practices

Learn proven patterns and best practices for building production-ready, scalable REST APIs with Node.js and Express.

BigXStar Team··10 min read
Node.jsAPIBackendJavaScript

Building Scalable APIs with Node.js: Best Practices

Building an API is easy. Building one that scales to thousands of concurrent users while remaining maintainable — that's the challenge. Here are the patterns that work in production.


Project Structure

A scalable API starts with proper organization:

src/
├── config/          # Configuration and environment
├── controllers/     # Request handlers
├── middleware/       # Auth, validation, error handling
├── models/          # Database models
├── routes/          # Route definitions
├── services/        # Business logic
├── utils/           # Helper functions
└── app.js           # App entry point

1. Separate Routes, Controllers, and Services

javascript
// routes/users.js
router.get("/users", userController.getAll);
router.post("/users", validate(userSchema), userController.create);

// controllers/userController.js
async function getAll(req, res, next) {
  try {
    const users = await userService.findAll(req.query);
    res.json({ data: users });
  } catch (error) {
    next(error);
  }
}

// services/userService.js
async function findAll(filters) {
  return User.find(filters).select("-password").lean();
}

2. Centralized Error Handling

javascript
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  const status = err.statusCode || 500;
  const message = err.isOperational ? err.message : "Internal server error";

  console.error(`[${status}] ${err.message}`, {
    path: req.path,
    method: req.method,
    stack: err.stack,
  });

  res.status(status).json({
    error: { message, status },
  });
}

3. Input Validation

Never trust client input:

javascript
import Joi from "joi";

const userSchema = Joi.object({
  name: Joi.string().min(2).max(100).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(18).max(120),
});

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

4. Rate Limiting and Security

javascript
import rateLimit from "express-rate-limit";
import helmet from "helmet";
import cors from "cors";

app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requests per window
  message: { error: "Too many requests" },
}));

5. Database Connection Pooling

javascript
import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,           // Maximum connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

6. Caching Strategy

javascript
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);

async function getCachedData(key, fetchFn, ttl = 3600) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const fresh = await fetchFn();
  await redis.setex(key, ttl, JSON.stringify(fresh));
  return fresh;
}

Key Takeaways

  • Structure matters — Separate concerns from day one
  • Validate everything — Never trust client input
  • Handle errors globally — Consistent error responses
  • Secure by default — Helmet, CORS, rate limiting
  • Cache aggressively — Redis for frequently accessed data
  • Monitor everything — Logging, metrics, and alerting

Learn Node.js API development in our web development courses with hands-on projects and mentorship.