TechLead
Lesson 7 of 9
5 min read
Web Security

API Security Best Practices

Secure your APIs with authentication, rate limiting, input validation, and proper error handling.

API Security Fundamentals

APIs are often the most exposed part of your application. A secure API requires multiple layers of protection.

API Authentication

1. API Keys

// Simple API key authentication
function apiKeyAuth(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' });
  }

  // Validate key (use timing-safe comparison)
  const validKey = await db.apiKey.findUnique({
    where: { key: hashApiKey(apiKey) },
    include: { user: true },
  });

  if (!validKey || validKey.revoked) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  req.user = validKey.user;
  req.apiKey = validKey;
  next();
}

// Generate secure API keys
function generateApiKey() {
  return crypto.randomBytes(32).toString('hex');
}

// Store hashed keys
function hashApiKey(key) {
  return crypto.createHash('sha256').update(key).digest('hex');
}

2. OAuth 2.0 / JWT

// JWT authentication middleware
async function jwtAuth(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Bearer token required' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);

    // Check token is not revoked
    const isRevoked = await isTokenRevoked(payload.jti);
    if (isRevoked) {
      return res.status(401).json({ error: 'Token revoked' });
    }

    req.user = payload;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Rate Limiting

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

// Different limits for different endpoints
const publicLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  message: { error: 'Too many requests' },
});

const authLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many authentication attempts' },
});

// Per-user rate limiting
const userLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 60 * 1000,
  max: 60,
  keyGenerator: (req) => req.user?.id || req.ip,
  message: { error: 'Rate limit exceeded' },
});

app.use('/api', publicLimiter);
app.use('/api/auth', authLimiter);
app.use('/api/protected', jwtAuth, userLimiter);

Input Validation

import { z } from 'zod';

// Define schemas
const createUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100),
  age: z.number().int().min(0).max(150).optional(),
});

const paginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(['asc', 'desc']).default('desc'),
});

// Validation middleware
function validate(schema) {
  return (req, res, next) => {
    try {
      req.validated = schema.parse({
        ...req.body,
        ...req.query,
        ...req.params,
      });
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({
          error: 'Validation failed',
          details: error.errors,
        });
      }
      next(error);
    }
  };
}

app.post('/api/users', validate(createUserSchema), createUser);
app.get('/api/users', validate(paginationSchema), listUsers);

Secure Error Handling

// Custom error classes
class AppError extends Error {
  constructor(message, statusCode, code) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404, 'NOT_FOUND');
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

// Error handling middleware
function errorHandler(err, req, res, next) {
  // Log error internally
  logger.error({
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    userId: req.user?.id,
  });

  // Don't leak internal errors to client
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
    });
  }

  // Generic error for unexpected errors
  res.status(500).json({
    error: 'Internal server error',
    code: 'INTERNAL_ERROR',
  });
}

app.use(errorHandler);

CORS Configuration

const cors = require('cors');

// Strict CORS configuration
const corsOptions = {
  origin: (origin, callback) => {
    const allowedOrigins = [
      'https://myapp.com',
      'https://admin.myapp.com',
    ];

    // Allow requests with no origin (mobile apps, curl)
    if (!origin) return callback(null, true);

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
  maxAge: 86400, // 24 hours
};

app.use(cors(corsOptions));

Request Sanitization

const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');

// Prevent NoSQL injection
app.use(mongoSanitize());

// Sanitize user input
app.use(xss());

// Custom sanitization
function sanitizeInput(obj) {
  if (typeof obj === 'string') {
    return obj.replace(/[<>]/g, '');
  }
  if (typeof obj === 'object' && obj !== null) {
    return Object.fromEntries(
      Object.entries(obj).map(([key, value]) => [key, sanitizeInput(value)])
    );
  }
  return obj;
}

app.use((req, res, next) => {
  req.body = sanitizeInput(req.body);
  req.query = sanitizeInput(req.query);
  next();
});

API Security Checklist

  • Use HTTPS for all endpoints
  • Implement proper authentication (API keys, JWT, OAuth)
  • Apply rate limiting per-user and per-endpoint
  • Validate all input with strict schemas
  • Return generic errors (don't leak internals)
  • Configure CORS properly
  • Sanitize input to prevent injection
  • Log all API access for audit
  • Use API versioning
  • Implement request signing for sensitive operations

Continue Learning