← Back to Guides

API Security Best Practices

πŸ“– 18 min read | πŸ“… Updated: January 2025 | 🏷️ Backend & APIs

Introduction

API security is critical for protecting user data and preventing attacks. This guide covers authentication, authorization, input validation, protection against common vulnerabilities (SQL injection, XSS, CSRF), rate limiting, and security best practices for production APIs.

1. OWASP API Security Top 10

OWASP API Security Top 10 (2023):

1. Broken Object Level Authorization (BOLA)
   - Users can access objects they shouldn't
   
2. Broken Authentication
   - Weak authentication implementation
   
3. Broken Object Property Level Authorization
   - Mass assignment vulnerabilities
   
4. Unrestricted Resource Consumption
   - No rate limiting, DDoS attacks
   
5. Broken Function Level Authorization
   - Users can access admin functions
   
6. Unrestricted Access to Sensitive Business Flows
   - Automated abuse (scalping, etc.)
   
7. Server Side Request Forgery (SSRF)
   - API makes unvalidated requests
   
8. Security Misconfiguration
   - Default passwords, verbose errors
   
9. Improper Inventory Management
   - Undocumented endpoints, old versions
   
10. Unsafe Consumption of APIs
    - Trusting third-party APIs without validation

2. Authentication & JWT Security

// Secure JWT implementation
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';

// Strong secret (use env variable)
const JWT_SECRET = process.env.JWT_SECRET; // min 256 bits
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

// Generate tokens
function generateTokens(userId: string) {
    const accessToken = jwt.sign(
        { userId, type: 'access' },
        JWT_SECRET,
        { 
            expiresIn: '15m', // Short-lived
            algorithm: 'HS256',
            issuer: 'my-api',
            audience: 'my-app'
        }
    );
    
    const refreshToken = jwt.sign(
        { userId, type: 'refresh' },
        JWT_REFRESH_SECRET,
        { 
            expiresIn: '7d',
            algorithm: 'HS256',
            issuer: 'my-api'
        }
    );
    
    return { accessToken, refreshToken };
}

// Verify with proper error handling
function verifyToken(token: string) {
    try {
        const decoded = jwt.verify(token, JWT_SECRET, {
            algorithms: ['HS256'],
            issuer: 'my-api',
            audience: 'my-app'
        });
        return decoded;
    } catch (error) {
        if (error instanceof jwt.TokenExpiredError) {
            throw new Error('Token expired');
        } else if (error instanceof jwt.JsonWebTokenError) {
            throw new Error('Invalid token');
        }
        throw error;
    }
}

// Login endpoint with rate limiting
import rateLimit from 'express-rate-limit';

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

app.post('/api/login', loginLimiter, async (req, res) => {
    const { email, password } = req.body;
    
    // Input validation
    if (!email || !password) {
        return res.status(400).json({ error: 'Email and password required' });
    }
    
    // Find user
    const user = await User.findOne({ email });
    if (!user) {
        // Generic error (don't reveal if user exists)
        return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Verify password with timing-safe comparison
    const isValid = await bcrypt.compare(password, user.passwordHash);
    if (!isValid) {
        // Log failed attempt
        await logFailedLogin(user.id, req.ip);
        return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Generate tokens
    const { accessToken, refreshToken } = generateTokens(user.id);
    
    // Store refresh token securely
    await saveRefreshToken(user.id, refreshToken);
    
    // Send tokens
    res.cookie('refreshToken', refreshToken, {
        httpOnly: true,
        secure: true, // HTTPS only
        sameSite: 'strict',
        maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
    });
    
    res.json({ accessToken, user: { id: user.id, email: user.email } });
});

3. Authorization & RBAC

// Role-Based Access Control
enum Role {
    USER = 'user',
    ADMIN = 'admin',
    MODERATOR = 'moderator'
}

interface AuthRequest extends Request {
    user?: {
        id: string;
        role: Role;
        permissions: string[];
    };
}

// Authorization middleware
function authorize(...allowedRoles: Role[]) {
    return (req: AuthRequest, res: Response, next: NextFunction) => {
        if (!req.user) {
            return res.status(401).json({ error: 'Unauthorized' });
        }
        
        if (!allowedRoles.includes(req.user.role)) {
            return res.status(403).json({ error: 'Forbidden' });
        }
        
        next();
    };
}

// Permission-based authorization
function requirePermission(...permissions: string[]) {
    return (req: AuthRequest, res: Response, next: NextFunction) => {
        if (!req.user) {
            return res.status(401).json({ error: 'Unauthorized' });
        }
        
        const hasPermission = permissions.every(p => 
            req.user!.permissions.includes(p)
        );
        
        if (!hasPermission) {
            return res.status(403).json({ error: 'Insufficient permissions' });
        }
        
        next();
    };
}

// Usage
app.delete('/api/users/:id', 
    authenticate,
    authorize(Role.ADMIN),
    async (req, res) => {
        // Only admins can delete users
    }
);

app.post('/api/posts/:id/publish',
    authenticate,
    requirePermission('posts:publish'),
    async (req, res) => {
        // User needs specific permission
    }
);

// Object-level authorization (BOLA protection)
app.get('/api/posts/:id', authenticate, async (req, res) => {
    const post = await Post.findById(req.params.id);
    
    if (!post) {
        return res.status(404).json({ error: 'Post not found' });
    }
    
    // Check ownership or permissions
    if (post.authorId !== req.user!.id && !post.isPublic) {
        return res.status(403).json({ error: 'Access denied' });
    }
    
    res.json(post);
});

4. Input Validation & Sanitization

// Using Joi for validation
import Joi from 'joi';

const userSchema = Joi.object({
    email: Joi.string().email().required(),
    name: Joi.string().min(2).max(100).required(),
    age: Joi.number().integer().min(13).max(120),
    password: Joi.string()
        .min(8)
        .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
        .required()
        .messages({
            'string.pattern.base': 'Password must contain uppercase, lowercase, number and special character'
        })
});

// Validation middleware
function validate(schema: Joi.Schema) {
    return (req: Request, res: Response, next: NextFunction) => {
        const { error, value } = schema.validate(req.body, {
            abortEarly: false,
            stripUnknown: true // Remove unknown fields
        });
        
        if (error) {
            const errors = error.details.map(detail => ({
                field: detail.path.join('.'),
                message: detail.message
            }));
            
            return res.status(400).json({ errors });
        }
        
        req.body = value; // Use validated data
        next();
    };
}

// Usage
app.post('/api/users', validate(userSchema), async (req, res) => {
    // req.body is validated and sanitized
    const user = await User.create(req.body);
    res.json(user);
});

// SQL Injection prevention with parameterized queries
// BAD - Vulnerable to SQL injection
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = '${userId}'`;
db.query(query); // NEVER DO THIS

// GOOD - Use parameterized queries
const userId = req.params.id;
const query = 'SELECT * FROM users WHERE id = $1';
db.query(query, [userId]);

// NoSQL Injection prevention
// BAD - MongoDB injection
const email = req.body.email;
User.findOne({ email: email }); // If email = { $ne: null }, returns any user

// GOOD - Validate and sanitize
const userSchema = Joi.object({
    email: Joi.string().email().required()
});
const { value } = userSchema.validate({ email: req.body.email });
User.findOne({ email: value.email });

// XSS prevention - sanitize HTML
import DOMPurify from 'isomorphic-dompurify';

const sanitizedHtml = DOMPurify.sanitize(req.body.content, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href']
});

5. Rate Limiting & DDoS Protection

// Express rate limiting
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

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

// Global rate limit
const globalLimiter = rateLimit({
    store: new RedisStore({
        client: redis,
        prefix: 'rl:global:'
    }),
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // 100 requests per window
    message: 'Too many requests, please try again later',
    standardHeaders: true,
    legacyHeaders: false,
    handler: (req, res) => {
        res.status(429).json({
            error: 'Too many requests',
            retryAfter: req.rateLimit.resetTime
        });
    }
});

// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
    store: new RedisStore({
        client: redis,
        prefix: 'rl:auth:'
    }),
    windowMs: 15 * 60 * 1000,
    max: 5,
    skipSuccessfulRequests: true // Don't count successful logins
});

// Per-user rate limiting
const createUserLimiter = (userId: string) => {
    return rateLimit({
        store: new RedisStore({
            client: redis,
            prefix: `rl:user:${userId}:`
        }),
        windowMs: 60 * 1000, // 1 minute
        max: 10,
        keyGenerator: () => userId
    });
};

// Sliding window rate limiting (more accurate)
class SlidingWindowRateLimiter {
    constructor(
        private redis: Redis,
        private maxRequests: number,
        private windowMs: number
    ) {}
    
    async isAllowed(key: string): Promise {
        const now = Date.now();
        const windowStart = now - this.windowMs;
        
        // Remove old entries
        await this.redis.zremrangebyscore(key, 0, windowStart);
        
        // Count requests in window
        const count = await this.redis.zcard(key);
        
        if (count >= this.maxRequests) {
            return false;
        }
        
        // Add current request
        await this.redis.zadd(key, now, `${now}-${Math.random()}`);
        await this.redis.expire(key, Math.ceil(this.windowMs / 1000));
        
        return true;
    }
}

// Usage
app.use('/api/', globalLimiter);
app.post('/api/login', authLimiter, loginHandler);

// Request size limiting
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

6. CORS & CSRF Protection

// CORS configuration
import cors from 'cors';

const corsOptions = {
    origin: (origin, callback) => {
        const allowedOrigins = [
            'https://myapp.com',
            'https://www.myapp.com'
        ];
        
        // Allow requests with no origin (mobile apps, curl, etc.)
        if (!origin || allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    credentials: true, // Allow cookies
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    exposedHeaders: ['X-Total-Count'],
    maxAge: 86400 // 24 hours
};

app.use(cors(corsOptions));

// CSRF Protection with tokens
import csrf from 'csurf';

const csrfProtection = csrf({ 
    cookie: {
        httpOnly: true,
        secure: true,
        sameSite: 'strict'
    }
});

// Get CSRF token
app.get('/api/csrf-token', csrfProtection, (req, res) => {
    res.json({ csrfToken: req.csrfToken() });
});

// Protect state-changing operations
app.post('/api/posts', csrfProtection, async (req, res) => {
    // CSRF token validated automatically
    const post = await Post.create(req.body);
    res.json(post);
});

// SameSite cookies (alternative CSRF protection)
res.cookie('token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict', // or 'lax'
    maxAge: 3600000
});

7. Security Headers

// Using Helmet for security headers
import helmet from 'helmet';

app.use(helmet());

// Custom security headers
app.use((req, res, next) => {
    // Prevent clickjacking
    res.setHeader('X-Frame-Options', 'DENY');
    
    // XSS protection
    res.setHeader('X-Content-Type-Options', 'nosniff');
    
    // Force HTTPS
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    
    // Content Security Policy
    res.setHeader('Content-Security-Policy', 
        "default-src 'self'; " +
        "script-src 'self' 'unsafe-inline'; " +
        "style-src 'self' 'unsafe-inline'; " +
        "img-src 'self' data: https:;"
    );
    
    // Referrer policy
    res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
    
    // Permissions policy
    res.setHeader('Permissions-Policy', 
        'geolocation=(), microphone=(), camera=()'
    );
    
    next();
});

// Hide server information
app.disable('x-powered-by');

8. API Key Management

// API key authentication
import crypto from 'crypto';

// Generate API key
function generateApiKey(): string {
    return crypto.randomBytes(32).toString('hex');
}

// Hash API key for storage
function hashApiKey(apiKey: string): string {
    return crypto.createHash('sha256').update(apiKey).digest('hex');
}

// Middleware to validate API key
async function validateApiKey(req: Request, res: Response, next: NextFunction) {
    const apiKey = req.headers['x-api-key'] as string;
    
    if (!apiKey) {
        return res.status(401).json({ error: 'API key required' });
    }
    
    // Hash and lookup
    const hashedKey = hashApiKey(apiKey);
    const keyData = await ApiKey.findOne({ keyHash: hashedKey });
    
    if (!keyData) {
        return res.status(401).json({ error: 'Invalid API key' });
    }
    
    // Check if key is active
    if (!keyData.isActive) {
        return res.status(403).json({ error: 'API key deactivated' });
    }
    
    // Check rate limits for this key
    const allowed = await checkRateLimit(keyData.id);
    if (!allowed) {
        return res.status(429).json({ error: 'Rate limit exceeded' });
    }
    
    // Attach key data to request
    req.apiKey = keyData;
    
    // Log usage
    await logApiKeyUsage(keyData.id, req.path);
    
    next();
}

// API Key model
interface ApiKey {
    id: string;
    userId: string;
    keyHash: string;
    name: string;
    isActive: boolean;
    rateLimit: number;
    permissions: string[];
    createdAt: Date;
    lastUsedAt: Date;
}

// Usage
app.get('/api/public/data', validateApiKey, async (req, res) => {
    // Protected by API key
    res.json({ data: 'sensitive info' });
});

9. Error Handling & Logging

// Don't leak sensitive information in errors
// BAD
app.get('/api/users/:id', async (req, res) => {
    try {
        const user = await User.findById(req.params.id);
        res.json(user);
    } catch (error) {
        res.status(500).json({ error: error.message }); // Leaks info!
    }
});

// GOOD
class AppError extends Error {
    constructor(
        public statusCode: number,
        public message: string,
        public isOperational: boolean = true
    ) {
        super(message);
    }
}

// Global error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    // Log error with context
    logger.error('API Error', {
        error: err.message,
        stack: err.stack,
        path: req.path,
        method: req.method,
        ip: req.ip,
        userId: req.user?.id
    });
    
    // Send safe error to client
    if (err instanceof AppError && err.isOperational) {
        res.status(err.statusCode).json({
            error: err.message
        });
    } else {
        // Don't leak internal errors
        res.status(500).json({
            error: 'Internal server error'
        });
    }
});

// Structured logging
import winston from 'winston';

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

// Log security events
function logSecurityEvent(event: string, details: any) {
    logger.warn('Security Event', {
        event,
        ...details,
        timestamp: new Date().toISOString()
    });
}

10. Additional Security Measures

// HTTPS enforcement
app.use((req, res, next) => {
    if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
        return res.redirect(`https://${req.hostname}${req.url}`);
    }
    next();
});

// Request ID for tracing
import { v4 as uuidv4 } from 'uuid';

app.use((req, res, next) => {
    req.id = req.headers['x-request-id'] as string || uuidv4();
    res.setHeader('X-Request-ID', req.id);
    next();
});

// Timeout handling
import timeout from 'connect-timeout';

app.use(timeout('30s'));
app.use((req, res, next) => {
    if (!req.timedout) next();
});

// Parameter pollution prevention
import hpp from 'hpp';

app.use(hpp());

// Dependency security scanning
// package.json scripts
{
  "scripts": {
    "audit": "npm audit",
    "audit:fix": "npm audit fix"
  }
}

// Use Snyk or Dependabot for continuous monitoring

11. Security Checklist

βœ“ API Security Checklist:
⚠️ Common Security Mistakes:

Conclusion

API security requires defense in depth. Implement authentication, authorization, input validation, rate limiting, and proper error handling. Stay updated on OWASP guidelines, audit dependencies regularly, and always assume inputs are malicious. Security is not a featureβ€”it's a requirement.

πŸ’‘ Pro Tip: Use security scanning tools in your CI/CD pipeline. Tools like Snyk, npm audit, and OWASP ZAP can catch vulnerabilities before they reach production. Automate security testing alongside unit and integration tests.