Home / Blog / Building Secure REST APIs with Express.js: Rate Limiting, Validation & Headers
Software Development

Building Secure REST APIs with Express.js: Rate Limiting, Validation & Headers

AJAjish Stephen August 8, 2026 8 min read
Building Secure REST APIs with Express.js: Rate Limiting, Validation & Headers

Express gives you almost nothing for free — no input validation, no rate limiting, no security headers, no sanitization. That flexibility is part of why it's popular, but it also means every API built on it is only as secure as the middleware you actually add. Here's a working setup covering the pieces that matter most.

Security headers with Helmet

Helmet sets a sensible baseline of HTTP security headers in one line — the Node equivalent of the nginx headers covered in an earlier post:

npminstall helmet

consthelmet= require('helmet');
app.use(helmet());

This sets headers like X-Content-Type-Options, X-Frame-Options, and a reasonable default Content-Security-Policy automatically — you can override any individual header afterward if your app needs something more specific.

Input validation with express-validator

Never trust request data. Validate and sanitize before it touches your business logic:

const{ body, validationResult } = require('express-validator');

app.post(
  '/users',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }),
    body('name').trim().escape().notEmpty(),
  ],
  (req, res) => {
    consterrors= validationResult(req);
    if(!errors.isEmpty()) {
      returnres.status(422).json({ errors: errors.array() });
    }
    // safe to use req.body here
  }
);

Rate limiting

express-rate-limit caps how many requests a single IP can make in a window — critical for login endpoints, password resets, and anything that could be brute-forced:

constrateLimit= require('express-rate-limit');

constloginLimiter= rateLimit({
  windowMs: 15 * 60 * 1000,// 15 minutes
  max: 5,
  message: 'Too many login attempts, please try again later.',
  standardHeaders: true,
  legacyHeaders: false,
});

app.post('/login', loginLimiter, loginHandler);

Apply a stricter limit to sensitive routes like login than to general read endpoints — one shared limit across the whole API is rarely the right tradeoff.

Safe error handling

Never let a raw error object reach the client in production:

app.use((err, req, res, next) => {
  console.error(err);// log the real details server-side

  constisDev= process.env.NODE_ENV !== 'production';

  res.status(err.status || 500).json({
    message: isDev ? err.message : 'Something went wrong.',
  });
});

In production, a client should only ever see a generic message — the stack trace, file paths, and internal error details stay in your logs, not in the response body.

CORS — configured, not wide open

💡It's tempting to slap cors() on with no options during development and forget about it. In production, explicitly whitelist the origins your API actually needs to serve — an open CORS policy on an authenticated API means any website on the internet can make credentialed requests to it from a logged-in user's browser.

Building an API that needs to hold up under real traffic and real attackers, not just a demo? This is exactly the kind of work I do as part of web development engagements.

Common questions

Does Express have any security built in by default?
Almost none. Express is intentionally minimal — it does not set security headers, rate limit requests, validate input, or sanitize anything on its own. Every protection has to be added explicitly through middleware.
Is express-validator enough, or do I need a schema library too?
express-validator handles most everyday request validation well. For larger APIs with complex nested payloads, a schema library like Zod or Joi often scales better.
Should rate limiting happen at the application layer or the infrastructure layer?
Ideally both. Infrastructure-level rate limiting stops traffic before it reaches your Node process. Application-level rate limiting adds finer-grained rules for specific routes or users.
Why is leaking stack traces in error responses a real risk?
A stack trace can reveal file paths, package versions, and internal logic — reconnaissance handed to an attacker for free. Production errors should return a generic message and log details server-side only.
Building a Node.js API that needs to be production-ready?
I build and secure backend systems across Node.js, Laravel, and Python.
Explore Web Development →
© Copyright 2024 Ajish Stephen