Skip to main content

Command Palette

Search for a command to run...

How to Secure API: Security Guide

Published
8 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Why Traditional API Security Approaches Fail in 2025

The security landscape has fundamentally shifted. Five years ago, placing APIs behind a VPN and using static API keys provided reasonable protection for internal services. Today's reality involves public-facing APIs serving mobile apps, third-party integrations, IoT devices, and AI agents—each with different security requirements and threat profiles.

Traditional approaches fail for specific reasons. Static API keys stored in client applications get extracted through reverse engineering within hours of deployment. Basic rate limiting based on IP addresses becomes ineffective when attackers use residential proxy networks spanning millions of addresses. Session-based authentication doesn't scale across distributed microservices architectures where services need to validate requests without database lookups. Certificate pinning breaks when you need to rotate certificates across global CDN networks.

The regulatory environment compounds these technical challenges. GDPR, CCPA, HIPAA, and emerging AI regulations require demonstrable security controls, audit trails, and data minimization. A single unsecured API endpoint exposing personal data can trigger fines reaching 4% of global revenue. Modern API security must provide cryptographic proof of compliance, not just best-effort protection.

Modern Architecture for Secure API Implementation

Securing APIs in 2025 requires a defense-in-depth strategy combining multiple security layers that fail gracefully. The architecture centers on zero-trust principles where every request is authenticated, authorized, and validated regardless of origin.

Authentication Layer: OAuth 2.1 with PKCE

OAuth 2.1 consolidates security best practices from OAuth 2.0 while deprecating insecure flows. The Proof Key for Code Exchange (PKCE) extension prevents authorization code interception attacks that plague mobile and single-page applications.

import { randomBytes, createHash } from 'crypto';
import { SignJWT, jwtVerify } from 'jose';

interface TokenPayload {
  sub: string;
  scope: string[];
  iat: number;
  exp: number;
  jti: string;
}

class OAuth21Server {
  private readonly issuer: string;
  private readonly privateKey: CryptoKey;
  private readonly publicKey: CryptoKey;
  private readonly authorizationCodes: Map<string, {
    clientId: string;
    codeChallenge: string;
    codeChallengeMethod: string;
    scope: string[];
    userId: string;
    expiresAt: number;
  }> = new Map();

  constructor(issuer: string, privateKey: CryptoKey, publicKey: CryptoKey) {
    this.issuer = issuer;
    this.privateKey = privateKey;
    this.publicKey = publicKey;
  }

  // Authorization endpoint with PKCE
  async createAuthorizationCode(
    clientId: string,
    codeChallenge: string,
    codeChallengeMethod: 'S256',
    scope: string[],
    userId: string
  ): Promise<string> {
    const code = randomBytes(32).toString('base64url');

    this.authorizationCodes.set(code, {
      clientId,
      codeChallenge,
      codeChallengeMethod,
      scope,
      userId,
      expiresAt: Date.now() + 600000 // 10 minutes
    });

    // Clean up expired codes
    this.cleanupExpiredCodes();

    return code;
  }

  // Token endpoint with PKCE verification
  async exchangeCodeForToken(
    code: string,
    clientId: string,
    codeVerifier: string
  ): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
    const authCode = this.authorizationCodes.get(code);

    if (!authCode) {
      throw new Error('Invalid authorization code');
    }

    if (authCode.expiresAt < Date.now()) {
      this.authorizationCodes.delete(code);
      throw new Error('Authorization code expired');
    }

    if (authCode.clientId !== clientId) {
      throw new Error('Client ID mismatch');
    }

    // Verify PKCE challenge
    const computedChallenge = createHash('sha256')
      .update(codeVerifier)
      .digest('base64url');

    if (computedChallenge !== authCode.codeChallenge) {
      throw new Error('Invalid code verifier');
    }

    // Delete code after successful exchange (single use)
    this.authorizationCodes.delete(code);

    // Generate access token
    const jti = randomBytes(16).toString('hex');
    const accessToken = await new SignJWT({
      sub: authCode.userId,
      scope: authCode.scope,
      jti
    } as TokenPayload)
      .setProtectedHeader({ alg: 'ES256', typ: 'JWT' })
      .setIssuer(this.issuer)
      .setAudience(clientId)
      .setIssuedAt()
      .setExpirationTime('15m')
      .sign(this.privateKey);

    // Generate refresh token (longer lived, stored securely)
    const refreshToken = randomBytes(32).toString('base64url');

    return {
      accessToken,
      refreshToken,
      expiresIn: 900 // 15 minutes
    };
  }

  async verifyAccessToken(token: string): Promise<TokenPayload> {
    const { payload } = await jwtVerify(token, this.publicKey, {
      issuer: this.issuer
    });

    return payload as TokenPayload;
  }

  private cleanupExpiredCodes(): void {
    const now = Date.now();
    for (const [code, data] of this.authorizationCodes.entries()) {
      if (data.expiresAt < now) {
        this.authorizationCodes.delete(code);
      }
    }
  }
}

This implementation prevents authorization code interception because attackers cannot generate the correct code verifier without access to the original random value. The short-lived access tokens limit exposure windows, while refresh tokens enable long-term sessions without storing sensitive credentials.

Rate Limiting and Throttling

Modern rate limiting must operate at multiple granularities simultaneously: per-IP, per-user, per-endpoint, and per-resource. Distributed rate limiting using Redis with sliding window counters provides accurate enforcement across multiple API gateway instances.

import { Redis } from 'ioredis';

interface RateLimitConfig {
  windowMs: number;
  maxRequests: number;
  keyPrefix: string;
}

class DistributedRateLimiter {
  private readonly redis: Redis;

  constructor(redisUrl: string) {
    this.redis = new Redis(redisUrl, {
      enableOfflineQueue: false,
      maxRetriesPerRequest: 1
    });
  }

  async checkRateLimit(
    identifier: string,
    config: RateLimitConfig
  ): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
    const key = `${config.keyPrefix}:${identifier}`;
    const now = Date.now();
    const windowStart = now - config.windowMs;

    // Use Redis pipeline for atomic operations
    const pipeline = this.redis.pipeline();

    // Remove old entries outside the window
    pipeline.zremrangebyscore(key, 0, windowStart);

    // Count requests in current window
    pipeline.zcard(key);

    // Add current request
    pipeline.zadd(key, now, `${now}:${Math.random()}`);

    // Set expiration
    pipeline.expire(key, Math.ceil(config.windowMs / 1000));

    const results = await pipeline.exec();

    if (!results) {
      throw new Error('Redis pipeline failed');
    }

    const count = results[1][1] as number;
    const allowed = count < config.maxRequests;
    const remaining = Math.max(0, config.maxRequests - count - 1);
    const resetAt = now + config.windowMs;

    if (!allowed) {
      // Remove the request we just added since it's not allowed
      await this.redis.zrem(key, `${now}:${Math.random()}`);
    }

    return { allowed, remaining, resetAt };
  }

  async checkMultipleRateLimits(
    identifier: string,
    configs: RateLimitConfig[]
  ): Promise<{ allowed: boolean; limitedBy?: string; resetAt?: number }> {
    for (const config of configs) {
      const result = await this.checkRateLimit(identifier, config);
      if (!result.allowed) {
        return {
          allowed: false,
          limitedBy: config.keyPrefix,
          resetAt: result.resetAt
        };
      }
    }
    return { allowed: true };
  }
}

// Usage in API middleware
async function rateLimitMiddleware(req: Request, res: Response, next: Function) {
  const limiter = new DistributedRateLimiter(process.env.REDIS_URL!);
  const userId = req.user?.id || req.ip;

  const result = await limiter.checkMultipleRateLimits(userId, [
    { windowMs: 60000, maxRequests: 100, keyPrefix: 'rl:user:minute' },
    { windowMs: 3600000, maxRequests: 1000, keyPrefix: 'rl:user:hour' },
    { windowMs: 86400000, maxRequests: 10000, keyPrefix: 'rl:user:day' }
  ]);

  if (!result.allowed) {
    res.status(429).json({
      error: 'Rate limit exceeded',
      limitedBy: result.limitedBy,
      resetAt: new Date(result.resetAt!).toISOString()
    });
    return;
  }

  next();
}

This sliding window approach provides more accurate rate limiting than fixed windows, preventing burst traffic at window boundaries. The multi-tier limits protect against both rapid automated attacks and sustained scraping attempts.

Request Validation and Input Sanitization

Every API request must undergo strict validation before processing. Schema validation using TypeScript's type system combined with runtime validation libraries prevents injection attacks and malformed data from reaching business logic.

import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';

// Define strict schemas for API inputs
const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9_-]+$/),
  password: z.string().min(12).max(128),
  profile: z.object({
    displayName: z.string().min(1).max(100),
    bio: z.string().max(500).optional()
  })
});

const PaginationSchema = z.object({
  page: z.coerce.number().int().min(1).max(1000).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
  order: z.enum(['asc', 'desc']).default('desc')
});

class InputValidator {
  static validate<T>(schema: z.ZodSchema<T>, data: unknown): T {
    try {
      return schema.parse(data);
    } catch (error) {
      if (error instanceof z.ZodError) {
        throw new ValidationError(
          'Invalid input',
          error.errors.map(e => ({
            field: e.path.join('.'),
            message: e.message
          }))
        );
      }
      throw error;
    }
  }

  static sanitizeHtml(input: string): string {
    return DOMPurify.sanitize(input, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
      ALLOWED_ATTR: ['href']
    });
  }

  static sanitizeObject<T extends Record<string, any>>(obj: T): T {
    const sanitized = {} as T;
    for (const [key, value] of Object.entries(obj)) {
      if (typeof value === 'string') {
        sanitized[key as keyof T] = this.sanitizeHtml(value) as T[keyof T];
      } else if (typeof value === 'object' && value !== null) {
        sanitized[key as keyof T] = this.sanitizeObject(value);
      } else {
        sanitized[key as keyof T] = value;
      }
    }
    return sanitized;
  }
}

class ValidationError extends Error {
  constructor(
    message: string,
    public readonly errors: Array<{ field: string; message: string }>
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

// API endpoint with validation
async function createUserEndpoint(req: Request, res: Response) {
  // Validate and sanitize input
  const validatedData = InputValidator.validate(CreateUserSchema, req.body);
  const sanitizedData = InputValidator.sanitizeObject(validatedData);

  // Additional business logic validation
  const existingUser = await db.users.findByEmail(sanitizedData.email);
  if (existingUser) {
    return res.status(409).json({ error: 'Email already registered' });
  }

  // Proceed with user creation
  const user = await createUser(sanitizedData);
  res.status(201).json({ id: user.id, username: user.username });
}

API Gateway Security Headers

Security headers provide defense-in-depth protection against common web vulnerabilities. Modern API gateways must set comprehensive security headers on every response.

function securityHeadersMiddleware(req: Request, res: Response, next: Function) {
  // Prevent MIME type sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');

  // Enable browser XSS protection
  res.setHeader('X-XSS-Protection', '1; mode=block');

  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');

  // Strict transport security (HTTPS only)
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );

  // Content Security Policy for API responses
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'none'; frame-ancestors 'none'"
  );

  // Referrer policy
  res.setHeader('Referrer-Policy', 'no-referrer');

  // Permissions policy
  res.setHeader(
    'Permissions-Policy',
    'geolocation=(), microphone=(), camera=()'
  );

  // Remove server identification
  res.removeHeader('X-Powered-By');

  next();
}

Threat Detection and Monitoring

Passive security controls aren't sufficient. Active threat detection identifies and blocks attacks in real-time using behavioral analysis and anomaly detection.

```typescript interface SecurityEvent { timestamp: number; userId?: string; ip: string; endpoint: string; method: string; statusCode: number; responseTime: number; userAgent: string; }

class ThreatDetector { private readonly redis: Redis; private readonly suspiciousPatterns = [ /(..|\/etc\/|\/proc\/|\/sys\/)/i, // Path traversal /(union|select|insert|update|delete|drop|create|alter)\s/i, // SQL injection /<script|javascript:|onerror=/i, // XSS attempts /\${|<%|<\?php/i // Template injection ];

constructor(redisUrl: string) { this.redis = new Redis(redisUrl); }

async analyzeRequest(event: SecurityEvent): Promise<{ threat: boolean; reason?: string; riskScore: number; }> { let riskScore = 0; const reasons: string[] = [];

// Check for suspicious patterns in URL const url = event.endpoint; for (const pattern of this.suspiciousPatterns) { if (pattern.test(url)) { riskScore += 50; reasons.push('Suspicious pattern in URL'); break; } }

// Analyze request frequency const requestCount = await this.getRecentRequestCount(event.ip, 60000); if (requestCount > 100) { riskScore += 30; reasons.push('Abnormal request frequency'); }

// Check for endpoint scanning behavior const uniqueEndpoints = await this.getUniqueEndpointsAccessed(event.ip, 300000); if (uniqueEndpoints > 50) { riskScore += 40; reasons.push('Endpoint scanning detected'); }

// Analyze error rate const errorRate = await this.getErrorRate(event.ip, 300000); if (errorRate > 0.5) { riskScore += 35; reasons.push('High error rate'); }

// Check for credential stuffing patterns if (event.endpoint.includes('/login') && event.statusCode === 401) { const failedLogins = await this.getFailedLoginCount(event.ip, 600000); if (failedLogins > 10) { riskScore += 60; reasons.push('Credential stuffing attempt'); } }

return { threat: riskScore >= 70, reason: reasons.join(', '), riskScore }; }

private async getRecentRequestCount(ip: string, windowMs: number): Promise { const key = threat:requests:${ip}; const now = Date.now(); await this.redis.zremrangebyscore(key, 0, now - windowMs); return await this.redis.zcard(key); }

private async getUniqueEndpointsAccessed(ip: string, windowMs: number): Promise { const key = threat:endpoints:${ip}; return await this.redis.scard(key); }

private async getErrorRate(ip: string, windowMs: number): Promise { const totalKey = threat:total:${ip}; const errorKey = `threat