DEV Community

Cover image for How to Prevent Common Security Vulnerabilities in REST APIs
Akshay Kurve
Akshay Kurve

Posted on

How to Prevent Common Security Vulnerabilities in REST APIs

Building APIs is easy. Building secure APIs is what separates a good developer from a production-ready engineer.

If you're working with REST APIs using Node.js, Express, Fastify, or any backend stack, security is not optional anymore. With API attacks increasing by 400% since 2023 according to the latest OWASP reports, even small mistakes can expose sensitive user data or bring down your entire system.


Table of Contents


1. Broken Authentication

The Problem

Weak authentication allows attackers to impersonate users. Common issues include:

  • Storing plain text passwords
  • Weak JWT secrets
  • No token expiration
  • Missing account lockout

Solution

Use Argon2 for password hashing (2026 standard):

const argon2 = require('argon2');

const hashingConfig = {
  type: argon2.argon2id,
  memoryCost: 2 ** 16, // 64 MB
  timeCost: 3,
  parallelism: 1
};

async function hashPassword(password) {
  return await argon2.hash(password, hashingConfig);
}

async function verifyPassword(password, hash) {
  return await argon2.verify(hash, password);
}
Enter fullscreen mode Exit fullscreen mode

Implement secure JWT with refresh tokens:

const jwt = require('jsonwebtoken');

function generateAccessToken(userId) {
  return jwt.sign(
    { userId, type: 'access' },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: '15m' }
  );
}

function generateRefreshToken(userId) {
  return jwt.sign(
    { userId, type: 'refresh', jti: crypto.randomBytes(16).toString('hex') },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );
}
Enter fullscreen mode Exit fullscreen mode

Add account lockout:

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true,
  message: 'Too many login attempts, please try again later'
});

app.post('/api/auth/login', loginLimiter, loginHandler);
Enter fullscreen mode Exit fullscreen mode

Implement Multi-Factor Authentication:

const speakeasy = require('speakeasy');

// Setup MFA
app.post('/api/auth/mfa/setup', authenticateToken, async (req, res) => {
  const secret = speakeasy.generateSecret({
    name: `YourApp (${req.user.email})`
  });

  await db.users.update(
    { mfaSecret: encrypt(secret.base32) },
    { where: { id: req.user.userId } }
  );

  const qrCode = await QRCode.toDataURL(secret.otpauth_url);
  res.json({ secret: secret.base32, qrCode });
});

// Verify MFA
app.post('/api/auth/mfa/verify', authenticateToken, async (req, res) => {
  const { token } = req.body;
  const user = await db.users.findByPk(req.user.userId);

  const isValid = speakeasy.totp.verify({
    secret: decrypt(user.mfaSecret),
    encoding: 'base32',
    token,
    window: 2
  });

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid MFA token' });
  }

  await db.users.update({ mfaEnabled: true }, { where: { id: req.user.userId } });
  res.json({ message: 'MFA enabled successfully' });
});
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


2. Broken Authorization

The Problem

Users accessing data they shouldn't - also known as IDOR (Insecure Direct Object References).

Vulnerable example:

// DANGEROUS
app.get('/api/users/:userId/profile', authenticateToken, async (req, res) => {
  const user = await db.users.findByPk(req.params.userId);
  res.json(user); // Any authenticated user can access any profile!
});
Enter fullscreen mode Exit fullscreen mode

Solution

Always verify ownership:

// SECURE
app.get('/api/users/:userId/profile', authenticateToken, async (req, res) => {
  const requestedUserId = parseInt(req.params.userId);
  const authenticatedUserId = req.user.userId;

  if (requestedUserId !== authenticatedUserId) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const user = await db.users.findByPk(requestedUserId);
  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

Implement Role-Based Access Control (RBAC):

function checkPermission(resource, action) {
  return async (req, res, next) => {
    const user = await db.users.findByPk(req.user.userId, {
      include: [{
        model: db.roles,
        include: [db.permissions]
      }]
    });

    const hasPermission = user.role.permissions.some(p =>
      p.resource === resource && p.action === action
    );

    if (!hasPermission) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    next();
  };
}

// Usage
app.get('/api/admin/users',
  authenticateToken,
  checkPermission('users', 'read'),
  async (req, res) => {
    const users = await db.users.findAll();
    res.json(users);
  }
);
Enter fullscreen mode Exit fullscreen mode

Create reusable authorization middleware:

function authorizeResourceOwner(resourceType, getResourceUserId) {
  return async (req, res, next) => {
    const authenticatedUserId = req.user.userId;
    const resourceUserId = await getResourceUserId(req);

    if (resourceUserId !== authenticatedUserId) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    next();
  };
}

// Usage
app.put('/api/posts/:postId',
  authenticateToken,
  authorizeResourceOwner('post', async (req) => {
    const post = await db.posts.findByPk(req.params.postId);
    return post ? post.userId : null;
  }),
  async (req, res) => {
    await db.posts.update(req.body, { where: { id: req.params.postId } });
    res.json({ message: 'Post updated' });
  }
);
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


3. Injection Attacks

SQL Injection

Vulnerable code:

// DANGEROUS
const query = `SELECT * FROM users WHERE email = '${email}'`;
const users = await db.raw(query);
Enter fullscreen mode Exit fullscreen mode

Attack example:

email = ' OR '1'='1
// Results in: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns all users!
Enter fullscreen mode Exit fullscreen mode

Solution

Always use parameterized queries:

// SECURE - Using placeholders
const users = await db.query(
  'SELECT * FROM users WHERE email = ?',
  [email]
);

// SECURE - Using ORM (Sequelize)
const users = await db.users.findAll({
  where: { email }
});

// SECURE - Using query builder (Knex)
const users = await knex('users')
  .where('email', email)
  .select('*');
Enter fullscreen mode Exit fullscreen mode

For dynamic queries, whitelist allowed values:

app.get('/api/users', async (req, res) => {
  const { sortBy, order } = req.query;

  const allowedSortColumns = ['name', 'email', 'created_at'];
  const allowedOrders = ['ASC', 'DESC'];

  if (!allowedSortColumns.includes(sortBy) || !allowedOrders.includes(order.toUpperCase())) {
    return res.status(400).json({ error: 'Invalid sort parameters' });
  }

  const users = await db.users.findAll({
    order: [[sortBy, order]]
  });

  res.json(users);
});
Enter fullscreen mode Exit fullscreen mode

NoSQL Injection

Vulnerable code:

// DANGEROUS
const user = await db.collection('users').findOne({
  email: email,
  password: password
});
Enter fullscreen mode Exit fullscreen mode

Attack payload:

{
  "email": {"$ne": null},
  "password": {"$ne": null}
}
Enter fullscreen mode Exit fullscreen mode

Solution

// SECURE - Validate input types
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;

  if (typeof email !== 'string' || typeof password !== 'string') {
    return res.status(400).json({ error: 'Invalid input format' });
  }

  if (!validator.isEmail(email)) {
    return res.status(400).json({ error: 'Invalid email format' });
  }

  const user = await db.collection('users').findOne({
    email: email.toString()
  });

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  res.json({ token: generateToken(user.id) });
});
Enter fullscreen mode Exit fullscreen mode

Command Injection

Vulnerable code:

// DANGEROUS
exec(`cat uploads/${filename}`, (error, stdout) => {
  res.send(stdout);
});
Enter fullscreen mode Exit fullscreen mode

Solution

// SECURE - Use fs APIs instead of shell commands
const fs = require('fs').promises;
const path = require('path');

app.get('/api/files/:filename', async (req, res) => {
  const { filename } = req.params;

  // Validate filename
  if (!/^[a-zA-Z0-9_\-\.]+$/.test(filename)) {
    return res.status(400).json({ error: 'Invalid filename' });
  }

  // Prevent path traversal
  const uploadsDir = path.resolve('./uploads');
  const filePath = path.join(uploadsDir, filename);

  if (!filePath.startsWith(uploadsDir)) {
    return res.status(400).json({ error: 'Invalid file path' });
  }

  try {
    const content = await fs.readFile(filePath, 'utf8');
    res.send(content);
  } catch (error) {
    res.status(404).json({ error: 'File not found' });
  }
});
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


4. Rate Limiting & DDoS Protection

Basic Rate Limiting

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

// General API rate limiter
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false
});

app.use('/api/', apiLimiter);

// Stricter for authentication
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true
});

app.post('/api/auth/login', authLimiter, loginHandler);
Enter fullscreen mode Exit fullscreen mode

Redis-Based Distributed Rate Limiting

const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT
});

const distributedLimiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:'
  }),
  windowMs: 60 * 1000,
  max: 100
});

app.use('/api/', distributedLimiter);
Enter fullscreen mode Exit fullscreen mode

Adaptive Rate Limiting

function createAdaptiveRateLimiter(options) {
  return async (req, res, next) => {
    let identifier;
    let limits;

    if (req.user) {
      identifier = `user:${req.user.userId}`;
      limits = req.user.isPremium ? options.premium : options.authenticated;
    } else {
      identifier = `ip:${req.ip}`;
      limits = options.anonymous;
    }

    const key = `rate_limit:${identifier}`;
    const count = await redis.incr(key);

    if (count === 1) {
      await redis.expire(key, Math.ceil(limits.windowMs / 1000));
    }

    if (count > limits.max) {
      return res.status(429).json({ error: 'Rate limit exceeded' });
    }

    res.set('X-RateLimit-Remaining', limits.max - count);
    next();
  };
}

const adaptiveLimit = createAdaptiveRateLimiter({
  anonymous: { windowMs: 15 * 60 * 1000, max: 20 },
  authenticated: { windowMs: 15 * 60 * 1000, max: 100 },
  premium: { windowMs: 15 * 60 * 1000, max: 1000 }
});
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


5. Data Exposure

The Problem

Returning too much data to the client, including:

  • Password hashes
  • Internal fields
  • Sensitive personal information
  • API keys and tokens

Vulnerable example:

// DANGEROUS
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findByPk(req.params.id);
  res.json(user); // Returns everything!
});

// Response includes:
{
  "id": 123,
  "email": "user@example.com",
  "password": "$2b$10$...",  // Exposed!
  "resetToken": "abc123",    // Exposed!
  "apiKey": "sk_live_..."    // Exposed!
}
Enter fullscreen mode Exit fullscreen mode

Solution

Use Data Transfer Objects (DTOs):

class UserDTO {
  constructor(user, options = {}) {
    this.id = user.id;
    this.email = user.email;
    this.name = user.name;
    this.createdAt = user.createdAt;

    if (options.includeProfile) {
      this.profile = {
        bio: user.bio,
        avatar: user.avatarUrl
      };
    }

    // Never include: password, resetToken, apiKey, internalNotes
  }

  static create(user, options) {
    return new UserDTO(user, options);
  }
}

app.get('/api/users/:id', authenticateToken, async (req, res) => {
  const user = await db.users.findByPk(req.params.id);
  const userDTO = UserDTO.create(user, { includeProfile: true });
  res.json(userDTO);
});
Enter fullscreen mode Exit fullscreen mode

Prevent Mass Assignment:

function sanitizeInput(data, allowedFields) {
  const sanitized = {};
  for (const field of allowedFields) {
    if (data.hasOwnProperty(field)) {
      sanitized[field] = data[field];
    }
  }
  return sanitized;
}

app.put('/api/users/:id', authenticateToken, async (req, res) => {
  if (parseInt(req.params.id) !== req.user.userId) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const allowedFields = ['name', 'bio', 'avatar', 'location'];
  const sanitizedData = sanitizeInput(req.body, allowedFields);

  await db.users.update(sanitizedData, { where: { id: req.user.userId } });
  res.json({ message: 'Profile updated' });
});
Enter fullscreen mode Exit fullscreen mode

Use Schema Validation:

const Joi = require('joi');

const updateUserSchema = Joi.object({
  name: Joi.string().min(2).max(50),
  bio: Joi.string().max(500),
  website: Joi.string().uri()
}).options({ stripUnknown: true });

app.put('/api/users/:id', authenticateToken, async (req, res) => {
  const { error, value } = updateUserSchema.validate(req.body);

  if (error) {
    return res.status(400).json({
      error: 'Validation failed',
      details: error.details.map(d => d.message)
    });
  }

  await db.users.update(value, { where: { id: req.user.userId } });
  res.json({ message: 'Profile updated' });
});
Enter fullscreen mode Exit fullscreen mode

Never put sensitive data in URLs:

// DANGEROUS
app.get('/api/reset-password', (req, res) => {
  const { token, newPassword } = req.query; // Logged everywhere!
});

// SECURE - Use POST with body
app.post('/api/reset-password', async (req, res) => {
  const { token, newPassword } = req.body; // Not in logs
  // Process password reset
});
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


6. Security Misconfiguration

Use Helmet for Security Headers

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:']
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// Remove server information
app.disable('x-powered-by');
Enter fullscreen mode Exit fullscreen mode

Configure CORS Properly

const cors = require('cors');

// DANGEROUS - Allow all
app.use(cors()); // DON'T DO THIS!

// SECURE - Whitelist origins
const allowedOrigins = [
  'https://yourdomain.com',
  'https://app.yourdomain.com'
];

if (process.env.NODE_ENV !== 'production') {
  allowedOrigins.push('http://localhost:3000');
}

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));
Enter fullscreen mode Exit fullscreen mode

Environment-Specific Configuration

// Disable debug features in production
app.use((err, req, res, next) => {
  console.error(err);

  if (process.env.NODE_ENV === 'production') {
    res.status(500).json({
      error: 'Internal server error'
      // Don't expose stack trace
    });
  } else {
    res.status(500).json({
      error: err.message,
      stack: err.stack
    });
  }
});

// Validate environment variables
const requiredEnvVars = [
  'NODE_ENV',
  'DATABASE_URL',
  'JWT_ACCESS_SECRET',
  'JWT_REFRESH_SECRET'
];

requiredEnvVars.forEach(varName => {
  if (!process.env[varName]) {
    console.error(`Missing required environment variable: ${varName}`);
    process.exit(1);
  }
});
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


7. Input Validation

Comprehensive Validation Middleware

const validator = require('validator');

function validateInput(schema) {
  return (req, res, next) => {
    const errors = [];

    for (const [field, rules] of Object.entries(schema)) {
      const value = req.body[field] || req.query[field];

      if (rules.required && !value) {
        errors.push(`${field} is required`);
        continue;
      }

      if (value) {
        if (rules.type && typeof value !== rules.type) {
          errors.push(`${field} must be a ${rules.type}`);
        }

        if (rules.isEmail && !validator.isEmail(value)) {
          errors.push(`${field} must be a valid email`);
        }

        if (rules.minLength && value.length < rules.minLength) {
          errors.push(`${field} must be at least ${rules.minLength} characters`);
        }

        if (rules.pattern && !rules.pattern.test(value)) {
          errors.push(`${field} has invalid format`);
        }

        if (rules.enum && !rules.enum.includes(value)) {
          errors.push(`${field} must be one of: ${rules.enum.join(', ')}`);
        }
      }
    }

    if (errors.length > 0) {
      return res.status(400).json({ errors });
    }

    next();
  };
}

// Usage
app.post('/api/users',
  validateInput({
    email: { required: true, type: 'string', isEmail: true },
    password: { required: true, type: 'string', minLength: 8 },
    age: { required: true, type: 'number', min: 18 },
    role: { type: 'string', enum: ['user', 'admin'] }
  }),
  createUserHandler
);
Enter fullscreen mode Exit fullscreen mode

Sanitize HTML Input

const sanitizeHtml = require('sanitize-html');

app.post('/api/posts', async (req, res) => {
  const { title, content } = req.body;

  const sanitizedContent = sanitizeHtml(content, {
    allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    allowedAttributes: {
      'a': ['href']
    }
  });

  const post = await db.posts.create({
    title,
    content: sanitizedContent
  });

  res.json(post);
});
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


8. Logging & Monitoring

What to Log

const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// Log important events
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    const user = await authenticateUser(email, password);

    logger.info('Successful login', {
      userId: user.id,
      email: user.email,
      ip: req.ip,
      userAgent: req.get('User-Agent')
    });

    res.json({ token: generateToken(user.id) });
  } catch (error) {
    logger.warn('Failed login attempt', {
      email,
      ip: req.ip,
      reason: error.message
    });

    res.status(401).json({ error: 'Invalid credentials' });
  }
});
Enter fullscreen mode Exit fullscreen mode

What NOT to Log

// DANGEROUS - Never log these
logger.info('User data', {
  password: user.password,        // ❌ Never log passwords
  creditCard: user.creditCard,    // ❌ Never log payment info
  ssn: user.ssn,                  // ❌ Never log PII
  apiKey: user.apiKey             // ❌ Never log secrets
});

// SECURE - Log safe information only
logger.info('User action', {
  userId: user.id,                // ✅ Safe
  action: 'profile_update',       // ✅ Safe
  ip: req.ip,                     // ✅ Safe
  timestamp: new Date()           // ✅ Safe
});
Enter fullscreen mode Exit fullscreen mode

Security Monitoring

// Monitor suspicious activity
async function monitorSecurityEvents(req, res, next) {
  const events = [];

  // Multiple failed attempts
  const failedAttempts = await redis.get(`failed_login:${req.ip}`);
  if (failedAttempts > 3) {
    events.push({ type: 'multiple_failed_logins', ip: req.ip });
  }

  // Unusual access patterns
  const accessCount = await redis.get(`access_count:${req.ip}`);
  if (accessCount > 1000) {
    events.push({ type: 'high_request_volume', ip: req.ip });
  }

  // Log security events
  if (events.length > 0) {
    logger.warn('Security events detected', {
      events,
      ip: req.ip,
      userAgent: req.get('User-Agent')
    });

    // Send alert if critical
    if (events.some(e => e.type === 'multiple_failed_logins')) {
      await sendSecurityAlert(events);
    }
  }

  next();
}

app.use(monitorSecurityEvents);
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


9. API Versioning Security

Maintain Security Patches for All Versions

// Version-specific security middleware
const securityMiddleware = {
  v1: [
    helmet(),
    rateLimit({ windowMs: 15 * 60 * 1000, max: 50 }) // Stricter for old version
  ],
  v2: [
    helmet(),
    rateLimit({ windowMs: 15 * 60 * 1000, max: 100 })
  ]
};

app.use('/api/v1', ...securityMiddleware.v1, v1Routes);
app.use('/api/v2', ...securityMiddleware.v2, v2Routes);
Enter fullscreen mode Exit fullscreen mode

Deprecation with Security in Mind

// Add deprecation warnings
app.use('/api/v1', (req, res, next) => {
  res.set({
    'Deprecation': 'true',
    'Sunset': 'Wed, 01 Jul 2027 00:00:00 GMT',
    'Link': '<https://api.example.com/docs/migration>; rel="deprecation"'
  });

  logger.warn('Deprecated API version used', {
    version: 'v1',
    ip: req.ip,
    endpoint: req.path
  });

  next();
});

// Eventually sunset old versions
app.use('/api/v1', (req, res, next) => {
  const sunsetDate = new Date('2027-07-01');

  if (new Date() > sunsetDate) {
    return res.status(410).json({
      error: 'API version no longer available',
      message: 'Please upgrade to v2',
      migrationGuide: 'https://api.example.com/docs/migration'
    });
  }

  next();
});
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


10. HTTPS and Transport Security

Force HTTPS

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect(301, `https://${req.headers.host}${req.url}`);
  }
  next();
});

// Or use express-sslify
const sslify = require('express-sslify');
app.use(sslify.HTTPS({ trustProtoHeader: true }));
Enter fullscreen mode Exit fullscreen mode

HSTS Headers

// Strict Transport Security
app.use((req, res, next) => {
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  next();
});
Enter fullscreen mode Exit fullscreen mode

TLS Configuration

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('path/to/private-key.pem'),
  cert: fs.readFileSync('path/to/certificate.pem'),
  // TLS 1.3 only (2026 standard)
  minVersion: 'TLSv1.3',
  // Strong cipher suites
  ciphers: [
    'TLS_AES_128_GCM_SHA256',
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256'
  ].join(':')
};

https.createServer(options, app).listen(443);
Enter fullscreen mode Exit fullscreen mode

Back to Table of Contents


Security Checklist

Use this checklist to ensure your API is secure:

Authentication & Authorization

  • [ ] Passwords hashed with Argon2 or bcrypt (salt rounds ≥ 12)
  • [ ] JWT tokens have short expiration (≤ 15 minutes for access tokens)
  • [ ] Refresh tokens implemented properly
  • [ ] MFA available for sensitive operations
  • [ ] Account lockout after failed login attempts (5 attempts)
  • [ ] Password reset tokens are cryptographically secure and expire
  • [ ] Authorization checks on every protected endpoint
  • [ ] RBAC or ABAC implemented for complex permissions

Data Protection

  • [ ] Sensitive data never in URLs or logs
  • [ ] DTOs used to control data exposure
  • [ ] Mass assignment prevented with input whitelisting
  • [ ] PII encrypted at rest
  • [ ] HTTPS enforced everywhere
  • [ ] Secure cookies (httpOnly, secure, sameSite)

Input Validation

  • [ ] All user input validated server-side
  • [ ] Parameterized queries used (no SQL injection)
  • [ ] NoSQL injection prevented with type checking
  • [ ] File uploads validated (type, size, content)
  • [ ] HTML sanitized for rich text inputs
  • [ ] Schema validation with Joi/Zod

Rate Limiting & DDoS

  • [ ] Rate limiting on all endpoints
  • [ ] Stricter limits on authentication endpoints
  • [ ] Distributed rate limiting with Redis
  • [ ] IP-based and user-based rate limiting
  • [ ] CAPTCHA on sensitive endpoints

Security Configuration

  • [ ] Helmet.js configured
  • [ ] CORS properly restricted
  • [ ] Security headers set (CSP, HSTS, X-Frame-Options)
  • [ ] Debug mode disabled in production
  • [ ] Error messages don't expose sensitive info
  • [ ] Dependencies regularly updated
  • [ ] Environment variables validated

Monitoring & Logging

  • [ ] Authentication events logged
  • [ ] Failed requests logged
  • [ ] Security events monitored
  • [ ] Sensitive data never logged
  • [ ] Centralized logging system
  • [ ] Alerts for suspicious activity
  • [ ] Regular security audits

API Design

  • [ ] API versioning implemented
  • [ ] Old versions have security patches
  • [ ] Deprecation policy in place
  • [ ] API documentation up to date
  • [ ] Security requirements documented

Infrastructure

  • [ ] TLS 1.3 enforced
  • [ ] Certificate auto-renewal configured
  • [ ] Database connections encrypted
  • [ ] Secrets stored in environment variables or vault
  • [ ] Principle of least privilege applied
  • [ ] Regular security scanning

Back to Table of Contents


Quick Reference: Common Packages

# Authentication
npm install bcrypt argon2 jsonwebtoken speakeasy qrcode

# Validation
npm install joi validator sanitize-html

# Security
npm install helmet cors express-rate-limit rate-limit-redis

# Monitoring
npm install winston pino

# Database
npm install sequelize pg mongoose
npm install @prisma/client

# Utils
npm install dotenv crypto-js ioredis
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Security is not something you "add later" - it must be built into your API from day one.

The 5 Golden Rules:

  1. Never trust user input - Always validate and sanitize
  2. Implement proper authentication - Use strong hashing, JWT, and MFA
  3. Verify authorization - Check permissions on every request
  4. Encrypt everything - HTTPS, encrypted data at rest
  5. Monitor and log - Know what's happening in your API

According to the 2026 Verizon Data Breach Report, 81% of breaches involve weak or stolen credentials, and 43% of breaches target web applications and APIs. Don't become a statistic.

Remember: Good APIs aren't just about features - they're about trust. Your users trust you with their data. Security is how you honor that trust.


Resources

Official Documentation:

Tools:

  • Snyk - Dependency vulnerability scanning
  • npm audit - Built-in vulnerability checker
  • OWASP ZAP - Security testing tool
  • Postman - API testing and documentation

Further Reading:

  • "Web Application Security" by Andrew Hoffman
  • "API Security in Action" by Neil Madden
  • IBM Security Report 2026
  • Verizon Data Breach Investigations Report 2026

About This Guide

This guide is based on industry best practices as of 2026, incorporating the latest OWASP recommendations, security standards, and real-world implementation patterns. Security is an ever-evolving field - stay informed and keep your dependencies updated.

For questions or suggestions, feel free to reach out or contribute to improving API security practices.


Top comments (0)