Back to catalog

api-security-patterns

This repository contains a complete **Cursor (Claude AI)** infrastructure that provides **auto-activating skills**, **specialized agents**, and **powerful commands** to help you build better applications faster.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

# API Security Patterns

**Skill Version:** 1.0.0  
**Stack:** Next.js API Routes, TypeScript  
**Purpose:** Production-ready API security including rate limiting, headers, and request validation

## Overview

This skill provides battle-tested patterns for securing Next.js API routes, including rate limiting, security headers, CORS configuration, input validation, and request sanitization.

## When to Use This Skill

Load this skill when working on:
- API route handlers
- Middleware for security
- Rate limiting implementation
- Security headers configuration
- API request validation
- Protected endpoints

## Quick Reference

### Core Security Layers
1. **Rate Limiting** - Prevent abuse and DOS attacks
2. **Security Headers** - HSTS, X-Frame-Options, CSP, etc.
3. **Input Validation** - Validate and sanitize all inputs
4. **Authentication** - Verify user identity
5. **Authorization** - Check user permissions
6. **Error Handling** - Don't leak sensitive information

## Core Guidelines

### 1. Rate Limiting Implementation

```typescript
// lib/security/rateLimit.ts

interface RateLimitStore {
  [key: string]: {
    count: number;
    resetTime: number;
  };
}

const store: RateLimitStore = {};

// Cleanup old entries every 5 minutes
setInterval(() => {
  const now = Date.now();
  Object.keys(store).forEach(key => {
    if (store[key].resetTime < now) {
      delete store[key];
    }
  });
}, 5 * 60 * 1000);

export interface RateLimitConfig {
  /**
   * Maximum number of requests allowed in the time window
   */
  maxRequests: number;
  
  /**
   * Time window in seconds
   */
  windowSeconds: number;
  
  /**
   * Custom identifier function (defaults to IP address)
   */
  identifier?: (request: Request) => string;
}

export interface RateLimitResult {
  success: boolean;
  limit: number;
  remaining: number;
  reset: number;
}

/**
 * Check if request is within rate limit
 */
export function checkRateLimit(
  request: Request,
  config: RateLimitConfig
): RateLimitResult {
  const identifier = config.identifier 
    ? config.identifier(request)
    : getClientIdentifier(request);

  const now = Date.now();
  const windowMs = config.windowSeconds * 1000;
  
  if (!store[identifier] || store[identifier].resetTime < now) {
    // First request or window expired
    store[identifier] = {
      count: 1,
      resetTime: now + windowMs,
    };
    
    return {
      success: true,
      limit: config.maxRequests,
      remaining: config.maxRequests - 1,
      reset: store[identifier].resetTime,
    };
  }

  // Increment count
  store[identifier].count++;

  const remaining = Math.max(0, config.maxRequests - store[identifier].count);
  const success = store[identifier].count <= config.maxRequests;

  return {
    success,
    limit: config.maxRequests,
    remaining,
    reset: store[identifier].resetTime,
  };
}

/**
 * Get client identifier (IP address)
 */
function getClientIdentifier(request: Request): string {
  const headers = request.headers;
  
  // Check for Cloud Run / Load Balancer forwarded IP
  const forwardedFor = headers.get('x-forwarded-for');
  if (forwardedFor) {
    return forwardedFor.split(',')[0].trim();
  }
  
  // Check for other common headers
  const realIp = headers.get('x-real-ip');
  if (realIp) {
    return realIp;
  }
  
  // Fallback to a constant (not ideal, but prevents crashes)
  return 'unknown';
}

/**
 * Predefined rate limit configurations
 */
export const RateLimitPresets = {
  // Strict limits for authentication endpoints
  AUTH: {
    maxRequests: 20,
    windowSeconds: 60 * 3, // 3 minutes
  },
  
  // Moderate limits for API endpoints
  API: {
    maxRequests: 100,
    windowSeconds: 60, // 1 minute
  },
  
  // Lenient limits for general pages
  PAGE: {
    maxRequests: 200,
    windowSeconds: 60, // 1 minute
  },
  
  // Very strict for password reset
  PASSWORD_RESET: {
    maxRequests: 10,
    windowSeconds: 60 * 60, // 1 hour
  },
  
  // Strict for data mutations
  MUTATION: {
    maxRequests: 30,
    windowSeconds: 60, // 1 minute
  },
} as const;

/**
 * Create rate limit response
 */
export function createRateLimitResponse(result: RateLimitResult): Response {
  return new Response(
    JSON.stringify({
      error: 'Too many requests',
      message: 'You have exceeded the rate limit. Please try again later.',
      limit: result.limit,
      remaining: result.remaining,
      reset: new Date(result.reset).toISOString(),
    }),
    {
      status: 429,
      headers: {
        'Content-Type': 'application/json',
        'X-RateLimit-Limit': result.limit.toString(),
        'X-RateLimit-Remaining': result.remaining.toString(),
        'X-RateLimit-Reset': result.reset.toString(),
        'Retry-After': Math.ceil((result.reset - Date.now()) / 1000).toString(),
      },
    }
  );
}
```

### 2. Security Headers Configuration

```typescript
// lib/security/headers.ts

export interface SecurityHeaders {
  'X-DNS-Prefetch-Control': string;
  'Strict-Transport-Security': string;
  'X-Frame-Options': string;
  'X-Content-Type-Options': string;
  'Referrer-Policy': string;
  'Permissions-Policy': string;
  'X-XSS-Protection': string;
}

/**
 * Get security headers for responses
 */
export function getSecurityHeaders(): SecurityHeaders {
  return {
    // Disable DNS prefetching
    'X-DNS-Prefetch-Control': 'off',
    
    // Enforce HTTPS (only in production)
    'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
    
    // Prevent clickjacking
    'X-Frame-Options': 'DENY',
    
    // Prevent MIME type sniffing
    'X-Content-Type-Options': 'nosniff',
    
    // Control referrer information
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    
    // Restrict browser features
    'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
    
    // XSS protection (legacy, but still useful)
    'X-XSS-Protection': '1; mode=block',
  };
}

/**
 * Apply security headers to a response
 */
export function applySecurityHeaders(response: Response): Response {
  const headers = getSecurityHeaders();
  
  Object.entries(headers).forEach(([key, value]) => {
    response.headers.set(key, value);
  });
  
  return response;
}

/**
 * Get CORS headers for API responses
 */
export function getCorsHeaders(origin?: string): Record<string, string> {
  // In production, you should whitelist specific origins
  const allowedOrigins = process.env.NEXT_PUBLIC_ALLOWED_ORIGINS?.split(',') || [];
  
  // Check if origin is allowed
  const isAllowed = allowedOrigins.includes('*') || 
    (origin && allowedOrigins.includes(origin));
  
  if (!isAllowed && allowedOrigins.length > 0) {
    return {};
  }
  
  return {
    'Access-Control-Allow-Origin': origin || '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Max-Age': '86400', // 24 hours
  };
}

/**
 * Handle CORS preflight requests
 */
export function handleCorsPrefligh(request: Request): Response {
  const origin = request.headers.get('origin') || undefined;
  const corsHeaders = getCorsHeaders(origin);
  
  return new Response(null, {
    status: 204,
    headers: corsHeaders,
  });
}
```

### 3. Protected API Route Pattern

```typescript
// app/api/orders/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { checkRateLimit, RateLimitPresets, createRateLimitResponse } from '@/lib/security/rateLimit';
import { applySecurityHeaders } from '@/lib/security/headers';
import { verifyAuth } from '@/lib/auth';
import { sanitizeObject } from '@/lib/validation';

export async function POST(request: NextRequest) {
  try {
    // 1. Rate Limiting
    const rateLimitResult = checkRateLimit(request, RateLimitPresets.MUTATION);
    if (!rateLimitResult.success) {
      return createRateLimitResponse(rateLimitResult);
    }

    // 2. Authentication
    const userId = await verifyAuth(request);
    if (!userId) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    // 3. Parse and validate body
    let body;
    try {
      body = await request.json();
    } catch {
      return NextResponse.json(
        { error: 'Invalid JSON body' },
        { status: 400 }
      );
    }

    // 4. Validate required fields
    const { amount, description, date } = body;
    if (!amount || !description || !date) {
      return NextResponse.json(
        { error: 'Missing required fields' },
        { status: 400 }
      );
    }

    // 5. Validate data types and ranges
    const numAmount = parseFloat(amount);
    if (isNaN(numAmount) || numAmount <= 0 || numAmount > 1000000) {
      return NextResponse.json(
        { error: 'Invalid amount' },
        { status: 400 }
      );
    }

    // 6. Sanitize input
    const sanitizedData = sanitizeObject({
      amount: numAmount,
      description,
      date,
      userId,
    });

    // 7. Process the request
    // const order = await createOrder(sanitizedData);

    // 8. Return success with security headers
    const response = NextResponse.json({
      success: true,
      data: { id: 'order-123' },
    });

    return applySecurityHeaders(response);

  } catch (error) {
    console.error('Error processing order:', error);
    
    // Don't leak error details to client
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

// Handle OPTIONS for CORS
export async function OPTIONS(request: NextRequest) {
  return handleCorsPrefligh(request);
}
```

### 4. Request Validation Middleware

```typescript
// lib/api/validation.ts
import { NextRequest, NextResponse } from 'next/server';
import { z, ZodSchema } from 'zod';

/**
 * Validate request body against a Zod schema
 */
export async function validateRequest<T>(
  request: NextRequest,
  schema: ZodSchema<T>
): Promise<{ success: true; data: T } | { success: false; response: NextResponse }> {
  try {
    const body = await request.json();
    const validatedData = schema.parse(body);
    return { success: true, data: validatedData };
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        success: false,
        response: NextResponse.json(
          {
            error: 'Validation failed',
            details: error.errors.map(err => ({
              field: err.path.join('.'),
              message: err.message,
            })),
          },
          { status: 400 }
        ),
      };
    }
    
    return {
      success: false,
      response: NextResponse.json(
        { error: 'Invalid request body' },
        { status: 400 }
      ),
    };
  }
}

/**
 * Check request body size
 */
export function validateBodySize(
  body: any,
  maxSizeKb: number = 100
): boolean {
  const bodyString = JSON.stringify(body);
  const sizeKb = new Blob([bodyString]).size / 1024;
  return sizeKb <= maxSizeKb;
}
```

### 5. API Error Handler Wrapper

```typescript
// lib/api/error-handler.ts
import { NextRequest, NextResponse } from 'next/server';

export class APIError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public code?: string
  ) {
    super(message);
    this.name = 'APIError';
  }
}

type Handler = (
  request: NextRequest,
  context?: any
) => Promise<NextResponse>;

/**
 * Wrap API route handler with error handling
 */
export function withErrorHandler(handler: Handler): Handler {
  return async (request: NextRequest, context?: any) => {
    try {
      return await handler(request, context);
    } catch (error) {
      console.error('API Error:', error);
      
      if (error instanceof APIError) {
        return NextResponse.json(
          { 
            error: error.message,
            code: error.code,
          },
          { status: error.statusCode }
        );
      }
      
      // Don't leak internal error details
      return NextResponse.json(
        { error: 'Internal server error' },
        { status: 500 }
      );
    }
  };
}

/**
 * Usage example
 */
// export const POST = withErrorHandler(async (request) => {
//   throw new APIError('Not found', 404, 'RESOURCE_NOT_FOUND');
// });
```

### 6. Complete Secure API Route Example

```typescript
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { 
  checkRateLimit, 
  RateLimitPresets, 
  createRateLimitResponse 
} from '@/lib/security/rateLimit';
import { applySecurityHeaders } from '@/lib/security/headers';
import { validateRequest } from '@/lib/api/validation';
import { withErrorHandler, APIError } from '@/lib/api/error-handler';
import { verifyAuth } from '@/lib/auth';
import { sanitizeObject } from '@/lib/validation';

// Define validation schema
const createProductSchema = z.object({
  sku: z.string().regex(/^PRD-\d{4,}$/),
  price: z.number().min(0.01).max(1000000),
  name: z.string().min(2).max(100),
  description: z.string().max(500),
  releaseDate: z.string().datetime(),
});

export const POST = withErrorHandler(async (request: NextRequest) => {
  // 1. Rate Limiting
  const rateLimitResult = checkRateLimit(request, RateLimitPresets.MUTATION);
  if (!rateLimitResult.success) {
    return createRateLimitResponse(rateLimitResult);
  }

  // 2. Authentication
  const userId = await verifyAuth(request);
  if (!userId) {
    throw new APIError('Unauthorized', 401, 'AUTH_REQUIRED');
  }

  // 3. Validate request body
  const validation = await validateRequest(request, createProductSchema);
  if (!validation.success) {
    return validation.response;
  }

  const { data } = validation;

  // 4. Additional business validation
  // Check if SKU already exists
  // const existingProduct = await checkProductExists(data.sku);
  // if (existingProduct) {
  //   throw new APIError('SKU already exists', 409, 'DUPLICATE_SKU');
  // }

  // 5. Sanitize data (extra safety)
  const sanitizedData = sanitizeObject({
    ...data,
    userId,
    createdAt: new Date().toISOString(),
  });

  // 6. Process request
  // const product = await createProduct(sanitizedData);

  // 7. Return success with security headers
  const response = NextResponse.json({
    success: true,
    data: {
      id: 'prd-123',
      ...sanitizedData,
    },
  }, { status: 201 });

  // Add rate limit headers
  response.headers.set('X-RateLimit-Limit', rateLimitResult.limit.toString());
  response.headers.set('X-RateLimit-Remaining', rateLimitResult.remaining.toString());

  return applySecurityHeaders(response);
});

// GET endpoint with pagination and filtering
export const GET = withErrorHandler(async (request: NextRequest) => {
  // Rate limiting
  const rateLimitResult = checkRateLimit(request, RateLimitPresets.API);
  if (!rateLimitResult.success) {
    return createRateLimitResponse(rateLimitResult);
  }

  // Authentication
  const userId = await verifyAuth(request);
  if (!userId) {
    throw new APIError('Unauthorized', 401, 'AUTH_REQUIRED');
  }

  // Parse query parameters
  const { searchParams } = new URL(request.url);
  const page = Math.max(1, parseInt(searchParams.get('page') || '1'));
  const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') || '20')));
  const category = searchParams.get('category') || undefined;

  // Fetch data
  // const products = await getProducts({ userId, page, limit, category });

  const response = NextResponse.json({
    data: [], // products,
    pagination: {
      page,
      limit,
      total: 0,
    },
  });

  return applySecurityHeaders(response);
});
```

### 7. Middleware for Global Security

```typescript
// middleware.ts (root level)
import { NextRequest, NextResponse } from 'next/server';
import { applySecurityHeaders } from '@/lib/security/headers';

export function middleware(request: NextRequest) {
  // Apply security headers to all responses
  const response = NextResponse.next();
  return applySecurityHeaders(response);
}

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};
```

## Security Best Practices

### Do's ✅
- **Always rate limit** sensitive endpoints (auth, mutations)
- **Validate all inputs** on both client and server
- **Use TypeScript** for type safety
- **Sanitize user input** before storing
- **Apply security headers** to all responses
- **Log security events** (rate limit hits, auth failures)
- **Use environment variables** for sensitive config
- **Implement CORS** properly for cross-origin requests
- **Return generic errors** to users (don't leak details)
- **Use HTTPS** in production

### Don'ts ❌
- **Don't trust client data** - always validate server-side
- **Don't expose stack traces** to users
- **Don't use weak rate limits** on critical endpoints
- **Don't skip authentication** checks
- **Don't log sensitive data** (passwords, tokens)
- **Don't allow unlimited request sizes**
- **Don't use `any` types** - be explicit
- **Don't forget OPTIONS** handler for CORS
- **Don't hard-code secrets** in code
- **Don't ignore error handling**

## Rate Limit Guidelines

| Endpoint Type | Suggested Limit | Window |
|--------------|----------------|--------|
| Authentication | 20 requests | 3 minutes |
| Password Reset | 10 requests | 1 hour |
| Data Mutations | 30 requests | 1 minute |
| Read Operations | 100 requests | 1 minute |
| Public Pages | 200 requests | 1 minute |
| File Uploads | 10 requests | 5 minutes |

## Security Checklist

- [ ] Rate limiting implemented
- [ ] Security headers configured
- [ ] Input validation on all endpoints
- [ ] Authentication required for protected routes
- [ ] Authorization checks in place
- [ ] Request body size limits
- [ ] CORS properly configured
- [ ] Error handling doesn't leak info
- [ ] Logging for security events
- [ ] HTTPS enforced in production

---

**Last Updated:** 2025-01-10  
**Maintained By:** Development Team