Skip to content

API Security Best Practices

Alex Stojcic edited this page Apr 3, 2025 · 2 revisions

API Security Best Practices

APIs (Application Programming Interfaces) are the connective tissue of modern applications. Securing APIs is critical as they often expose sensitive data and functionality. This guide covers comprehensive API security best practices to protect your services.

API Security Fundamentals

Types of API Vulnerabilities

  1. Authentication flaws - Weak or missing authentication mechanisms
  2. Authorization issues - Improper access controls
  3. Input validation gaps - Accepting and processing malicious input
  4. Excessive data exposure - Returning more data than necessary
  5. Resource constraints lacking - Allowing excessive usage or abuse
  6. Security misconfigurations - Insecure default settings
  7. Injection flaws - Command or query injection vulnerabilities
  8. Improper assets management - Unpatched or deprecated API versions

Authentication & Authorization

API Authentication Methods

Method Description Best For
API Keys Simple secret tokens Low-risk internal APIs, developer access
Bearer Tokens Tokens passed in headers Most API scenarios, especially JWTs
OAuth 2.0 Token delegation framework Complex permissions, third-party integrations
mTLS Mutual certificate authentication High-security environments

Implementing JWT Authentication

// Node.js/Express example with JWT
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

// Middleware to authenticate JWT function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1];

if (!token) return res.status(401).json({ error: 'Access token required' });

jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid or expired token' }); req.user = user; next(); }); }

// Protected route app.get('/api/protected-resource', authenticateToken, (req, res) => { res.json({ data: 'This is protected data', user: req.user }); });

// Token issuance (login) app.post('/api/login', (req, res) => { // Verify credentials (simplified) const user = authenticateUser(req.body.username, req.body.password);

if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); }

// Create tokens const accessToken = jwt.sign( { id: user.id, username: user.username, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' } );

const refreshToken = jwt.sign( { id: user.id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' } );

// Store refresh token securely (e.g., database) saveRefreshToken(user.id, refreshToken);

res.json({ accessToken, refreshToken, expiresIn: 900 // 15 minutes in seconds }); });

// Token refresh endpoint app.post('/api/refresh-token', (req, res) => { const refreshToken = req.body.refreshToken;

if (!refreshToken) { return res.status(401).json({ error: 'Refresh token required' }); }

// Verify token is valid and in our database if (!isValidRefreshToken(refreshToken)) { return res.status(403).json({ error: 'Invalid refresh token' }); }

jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid token' });

// Get full user data for new access token
const userData = getUserData(user.id);

const accessToken = jwt.sign(
  { id: userData.id, username: userData.username, role: userData.role },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }
);

res.json({ 
  accessToken, 
  expiresIn: 900 // 15 minutes in seconds
});

}); });

API Authorization with Role-Based Access Control

// Middleware for role-based authorization
function checkRole(roles) {
  return (req, res, next) => {
    // Get user from JWT authentication middleware
    const user = req.user;
if (!user) {
  return res.status(401).json({ error: 'Authentication required' });
}

if (!roles.includes(user.role)) {
  return res.status(403).json({ error: 'Insufficient permissions' });
}

next();

}; }

// Apply role checks to routes app.get('/api/users', authenticateToken, checkRole(['admin']), (req, res) => { // Only admins can access this endpoint res.json({ users: getAllUsers() }); } );

app.get('/api/products', authenticateToken, checkRole(['admin', 'manager', 'viewer']), (req, res) => { // Multiple roles can access this endpoint res.json({ products: getProducts() }); } );

Fine-Grained Permission Control

// Permission-based authorization (more flexible than roles)
function checkPermission(permission) {
  return async (req, res, next) => {
    const user = req.user;
if (!user) {
  return res.status(401).json({ error: 'Authentication required' });
}

// Get user permissions from database
const userPermissions = await getUserPermissions(user.id);

if (!userPermissions.includes(permission)) {
  return res.status(403).json({ error: 'Insufficient permissions' });
}

next();

}; }

// Apply permission checks app.post('/api/articles', authenticateToken, checkPermission('articles:create'), (req, res) => { // Create article logic } );

app.delete('/api/articles/:id', authenticateToken, checkPermission('articles:delete'), (req, res) => { // Delete article logic } );

API Input Validation

Comprehensive Request Validation

Using Express validator for Node.js:

const { body, param, query, validationResult } = require('express-validator');

app.post('/api/users', // Validate request body [ body('username') .isLength({ min: 3, max: 50 }).withMessage('Username must be 3-50 characters') .isAlphanumeric().withMessage('Username must contain only letters and numbers'),

body('email')
  .isEmail().withMessage('Must provide a valid email')
  .normalizeEmail(),

body('role')
  .isIn(['admin', 'user', 'manager']).withMessage('Invalid role specified')
  .optional(),

body('settings.*').optional() // Allow any settings object structure

], (req, res) => { // Check for validation errors const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); }

// Process valid input
createUser(req.body);
res.status(201).json({ message: 'User created successfully' });

} );

app.get('/api/products', // Validate query parameters [ query('page') .optional() .isInt({ min: 1 }).withMessage('Page must be a positive integer') .toInt(),

query('limit')
  .optional()
  .isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1-100')
  .toInt(),

query('sort')
  .optional()
  .isIn(['name', 'price', 'date']).withMessage('Invalid sort field')

], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); }

// Use validated and sanitized query params
const { page = 1, limit = 20, sort = 'date' } = req.query;
const products = getProducts(page, limit, sort);

res.json({ products });

} );

app.get('/api/products/:id', // Validate URL parameters param('id').isInt().withMessage('Product ID must be an integer'), (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); }

const product = getProductById(req.params.id);
if (!product) {
  return res.status(404).json({ error: 'Product not found' });
}

res.json({ product });

} );

API Data Filtering and Sanitization

// Sanitize and filter sensitive data
app.get('/api/users/:id', authenticateToken, async (req, res) => {
  const user = await getUserById(req.params.id);

if (!user) { return res.status(404).json({ error: 'User not found' }); }

// Filter out sensitive data based on requesting user's role const isAdmin = req.user.role === 'admin'; const isSameUser = req.user.id === user.id;

// Create sanitized response const sanitizedUser = { id: user.id, username: user.username, email: isSameUser || isAdmin ? user.email : undefined, role: isAdmin ? user.role : undefined, createdAt: user.createdAt, // Only include personal details if admin or same user ...(isSameUser || isAdmin ? { personalDetails: user.personalDetails } : {}) };

res.json({ user: sanitizedUser }); });

Rate Limiting and Resource Protection

API Rate Limiting

Using Express Rate Limit:

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

// Global rate limiter - applies to all endpoints const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs standardHeaders: true, // Return rate limit info in the RateLimit-* headers legacyHeaders: false, // Disable the X-RateLimit-* headers message: { error: 'Too many requests, please try again later.' } });

// Apply global rate limiting app.use(globalLimiter);

// Create specific limiters for sensitive endpoints const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 failed attempts per hour standardHeaders: true, skipSuccessfulRequests: true, // Only count failed requests message: { error: 'Too many failed login attempts, please try again later.' } });

// Apply to login endpoint app.post('/api/login', authLimiter, loginHandler);

// Create tier-based rate limits const freeTierLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour window max: 100, // 100 requests per hour keyGenerator: (req) => req.user.id, // Rate limit by user ID, not IP standardHeaders: true, message: { error: 'Rate limit exceeded for free tier.' } });

const premiumTierLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour window max: 1000, // 1000 requests per hour keyGenerator: (req) => req.user.id, standardHeaders: true, message: { error: 'Rate limit exceeded for premium tier.' } });

// Dynamic rate limiting based on user tier app.use('/api/data', authenticateToken, (req, res, next) => { if (req.user.tier === 'premium') { premiumTierLimiter(req, res, next); } else { freeTierLimiter(req, res, next); } });

Payload Size Limits

// Limit JSON body size
app.use(express.json({ limit: '10kb' }));

// Limit URL-encoded data app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// Custom file upload limits per route const multer = require('multer'); const upload = multer({ limits: { fileSize: 5 * 1024 * 1024 // 5MB } });

app.post('/api/upload/avatar', authenticateToken, upload.single('avatar'), (req, res) => { // Handle avatar upload } );

app.post('/api/upload/document', authenticateToken, upload.single('document'), (req, res) => { // Handle document upload } );

Secure Response Handling

HTTP Security Headers

const helmet = require('helmet');

// Apply security headers to all routes app.use(helmet());

// Configure specific headers app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "trusted-scripts.com"], objectSrc: ["'none'"], upgradeInsecureRequests: [] } }, crossOriginResourcePolicy: { policy: "same-site" }, crossOriginOpenerPolicy: { policy: "same-origin" } }));

// Add custom security headers app.use((req, res, next) => { res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); next(); });

CORS Configuration

const cors = require('cors');

// Basic CORS setup const corsOptions = { origin: 'https://yourapplication.com', // Restrict to your domain methods: ['GET', 'POST', 'PUT', 'DELETE'], // Allowed methods allowedHeaders: ['Content-Type', 'Authorization'], // Allowed headers exposedHeaders: ['Content-Range', 'X-Total-Count'], // Expose these headers credentials: true, // Allow cookies maxAge: 86400 // Cache preflight requests for 24 hours };

app.use(cors(corsOptions));

// Dynamic CORS based on environment if (process.env.NODE_ENV === 'development') { app.use(cors({ origin: '*', // Allow any origin in development credentials: true })); } else { app.use(cors({ origin: [ 'https://production-app.com', 'https://admin.production-app.com' ], credentials: true })); }

// Route-specific CORS app.options('/api/special-endpoint', cors({ origin: 'https://partner-site.com', methods: ['POST'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Special-Header'] }));

app.post('/api/special-endpoint', cors({ origin: 'https://partner-site.com', methods: ['POST'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Special-Header'] }), (req, res) => { // Handle special endpoint } );

API Logging and Monitoring

Secure API Logging

const winston = require('winston');
const expressWinston = require('express-winston');

// Create sanitized transport const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }) ] });

// Sensitive data patterns to redact const sensitiveFields = ['password', 'token', 'secret', 'authorization', 'cookie'];

// Custom sanitization function const sanitizeRequest = (req) => { const sanitized = { headers: { ...req.headers }, body: { ...req.body }, query: { ...req.query } };

// Sanitize headers sensitiveFields.forEach(field => { if (sanitized.headers[field]) { sanitized.headers[field] = '[REDACTED]'; }

// Case insensitive check for Authorization header
if (field === 'authorization' && sanitized.headers['Authorization']) {
  sanitized.headers['Authorization'] = '[REDACTED]';
}

});

// Sanitize body sensitiveFields.forEach(field => { if (sanitized.body && sanitized.body[field]) { sanitized.body[field] = '[REDACTED]'; } });

return sanitized; };

// HTTP request logger app.use(expressWinston.logger({ winstonInstance: logger, meta: true, // Sanitize request data before logging dynamicMeta: (req, res) => { return { request: sanitizeRequest(req), userId: req.user ? req.user.id : 'unauthenticated', ip: req.ip }; }, msg: 'HTTP {{req.method}} {{req.url}}', expressFormat: true, colorize: false }));

// Error logger app.use(expressWinston.errorLogger({ winstonInstance: logger, blacklistedMetaFields: ['exception', 'process', 'os'] }));

API Monitoring and Alerting

// Using Prometheus for API monitoring
const prometheus = require('prom-client');
const register = new prometheus.Registry();

// Add default metrics prometheus.collectDefaultMetrics({ register });

// Create custom metrics const httpRequestDurationMicroseconds = new prometheus.Histogram({ name: 'http_request_duration_ms', help: 'Duration of HTTP requests in ms', labelNames: ['method', 'route', 'status_code'], buckets: [10, 50, 100, 500, 1000, 5000] });

const httpRequestTotal = new prometheus.Counter({ name: 'http_requests_total', help: 'Total number of HTTP requests', labelNames: ['method', 'route', 'status_code'] });

const apiErrorTotal = new prometheus.Counter({ name: 'api_errors_total', help: 'Total number of API errors', labelNames: ['method', 'route', 'error_code'] });

// Register custom metrics register.registerMetric(httpRequestDurationMicroseconds); register.registerMetric(httpRequestTotal); register.registerMetric(apiErrorTotal);

// Middleware to record metrics app.use((req, res, next) => { const start = Date.now();

// Record when request ends res.on('finish', () => { const duration = Date.now() - start; const route = req.route ? req.route.path : req.path;

// Record request duration
httpRequestDurationMicroseconds
  .labels(req.method, route, res.statusCode)
  .observe(duration);

// Count request
httpRequestTotal
  .labels(req.method, route, res.statusCode)
  .inc();

// Count errors
if (res.statusCode >= 400) {
  apiErrorTotal
    .labels(req.method, route, res.statusCode)
    .inc();
}

});

next(); });

// Expose metrics endpoint for Prometheus scraping app.get('/metrics', async (req, res) => { res.set('Content-Type', register.contentType); res.end(await register.metrics()); });

API Versioning and Deprecation

// API versioning
const apiV1Router = express.Router();
const apiV2Router = express.Router();

// Set up v1 endpoints apiV1Router.get('/users', (req, res) => { // V1 implementation res.json({ users: getUsersV1() }); });

// Set up v2 endpoints apiV2Router.get('/users', (req, res) => { // V2 implementation with enhanced data res.json({ users: getUsersV2(), pagination: { totalCount: 100, page: 1, pageSize: 20 } }); });

// Mount versioned routers app.use('/api/v1', apiV1Router); app.use('/api/v2', apiV2Router);

// Deprecation warning middleware for v1 app.use('/api/v1', (req, res, next) => { res.setHeader('Warning', '299 - "Deprecated API version. Please migrate to v2 by July 2024"'); res.setHeader('X-API-Deprecated', 'true'); res.setHeader('X-API-Deprecation-Date', 'July 1, 2024'); res.setHeader('X-API-Migration-URL', 'https://api.example.com/docs/v2-migration'); next(); });

// Default to latest version app.use('/api', apiV2Router);

GraphQL API Security

const { ApolloServer } = require('apollo-server-express');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { applyMiddleware } = require('graphql-middleware');
const { rule, shield, allow, deny } = require('graphql-shield');

// Define GraphQL schema const typeDefs = ` type User { id: ID! username: String! email: String! role: String! sensitiveData: String }

type Query { me: User user(id: ID!): User users: [User] } `;

// Define resolvers const resolvers = { Query: { me: (, __, context) => { if (!context.user) { throw new Error('Not authenticated'); } return context.user; }, user: (, { id }, context) => { return getUserById(id); }, users: (_, __, context) => { return getAllUsers(); } } };

// Define permission rules const isAuthenticated = rule({ cache: 'contextual' })( async (_, __, context) => { return !!context.user; } );

const isAdmin = rule({ cache: 'contextual' })( async (_, __, context) => { return context.user && context.user.role === 'admin'; } );

const isSameUserOrAdmin = rule({ cache: 'contextual' })( async (_, args, context) => { if (!context.user) return false; if (context.user.role === 'admin') return true; return context.user.id === args.id; } );

// Define permissions const permissions = shield({ Query: { me: isAuthenticated, user: isSameUserOrAdmin, users: isAdmin }, User: { email: isAuthenticated, sensitiveData: isSameUserOrAdmin } });

// Create executable schema with permissions const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithPermissions = applyMiddleware(schema, permissions);

// Configure Apollo Server const server = new ApolloServer({ schema: schemaWithPermissions, context: ({ req }) => { // Authentication logic here const user = authenticateRequest(req); return { user }; }, // Set limits validationRules: [ depthLimit(5), // Limit query depth queryComplexity({ maximumComplexity: 1000, variables: {}, onComplete: (complexity) => { console.log('Query complexity:', complexity); } }) ], // Apollo Studio settings introspection: process.env.NODE_ENV !== 'production', playground: process.env.NODE_ENV !== 'production' });

// Apply Apollo middleware to Express server.applyMiddleware({ app });

Additional Resources

# API Security Best Practices

APIs (Application Programming Interfaces) are the connective tissue of modern applications. Securing APIs is critical as they often expose sensitive data and functionality. This guide covers comprehensive API security best practices to protect your services.

API Security Fundamentals

Types of API Vulnerabilities

  1. Authentication flaws - Weak or missing authentication mechanisms
  2. Authorization issues - Improper access controls
  3. Input validation gaps - Accepting and processing malicious input
  4. Excessive data exposure - Returning more data than necessary
  5. Resource constraints lacking - Allowing excessive usage or abuse
  6. Security misconfigurations - Insecure default settings
  7. Injection flaws - Command or query injection vulnerabilities
  8. Improper assets management - Unpatched or deprecated API versions

Authentication & Authorization

API Authentication Methods

Method Description Best For
API Keys Simple secret tokens Low-risk internal APIs, developer access
Bearer Tokens Tokens passed in headers Most API scenarios, especially JWTs
OAuth 2.0 Token delegation framework Complex permissions, third-party integrations
mTLS Mutual certificate authentication High-security environments

Implementing JWT Authentication

// Node.js/Express example with JWT
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

// Middleware to authenticate JWT
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) return res.status(401).json({ error: 'Access token required' });
  
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: 'Invalid or expired token' });
    req.user = user;
    next();
  });
}

// Protected route
app.get('/api/protected-resource', authenticateToken, (req, res) => {
  res.json({ data: 'This is protected data', user: req.user });
});

// Token issuance (login)
app.post('/api/login', (req, res) => {
  // Verify credentials (simplified)
  const user = authenticateUser(req.body.username, req.body.password);
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Create tokens
  const accessToken = jwt.sign(
    { id: user.id, username: user.username, role: user.role }, 
    process.env.JWT_SECRET, 
    { expiresIn: '15m' }
  );
  
  const refreshToken = jwt.sign(
    { id: user.id }, 
    process.env.REFRESH_TOKEN_SECRET, 
    { expiresIn: '7d' }
  );
  
  // Store refresh token securely (e.g., database)
  saveRefreshToken(user.id, refreshToken);
  
  res.json({ 
    accessToken, 
    refreshToken,
    expiresIn: 900 // 15 minutes in seconds
  });
});

// Token refresh endpoint
app.post('/api/refresh-token', (req, res) => {
  const refreshToken = req.body.refreshToken;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token required' });
  }
  
  // Verify token is valid and in our database
  if (!isValidRefreshToken(refreshToken)) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }
  
  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: 'Invalid token' });
    
    // Get full user data for new access token
    const userData = getUserData(user.id);
    
    const accessToken = jwt.sign(
      { id: userData.id, username: userData.username, role: userData.role },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );
    
    res.json({ 
      accessToken, 
      expiresIn: 900 // 15 minutes in seconds
    });
  });
});

API Authorization with Role-Based Access Control

// Middleware for role-based authorization
function checkRole(roles) {
  return (req, res, next) => {
    // Get user from JWT authentication middleware
    const user = req.user;
    
    if (!user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    
    if (!roles.includes(user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
}

// Apply role checks to routes
app.get('/api/users', 
  authenticateToken, 
  checkRole(['admin']), 
  (req, res) => {
    // Only admins can access this endpoint
    res.json({ users: getAllUsers() });
  }
);

app.get('/api/products', 
  authenticateToken, 
  checkRole(['admin', 'manager', 'viewer']),
  (req, res) => {
    // Multiple roles can access this endpoint
    res.json({ products: getProducts() });
  }
);

Fine-Grained Permission Control

// Permission-based authorization (more flexible than roles)
function checkPermission(permission) {
  return async (req, res, next) => {
    const user = req.user;
    
    if (!user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    
    // Get user permissions from database
    const userPermissions = await getUserPermissions(user.id);
    
    if (!userPermissions.includes(permission)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
}

// Apply permission checks
app.post('/api/articles', 
  authenticateToken,
  checkPermission('articles:create'),
  (req, res) => {
    // Create article logic
  }
);

app.delete('/api/articles/:id', 
  authenticateToken,
  checkPermission('articles:delete'),
  (req, res) => {
    // Delete article logic
  }
);

API Input Validation

Comprehensive Request Validation

Using Express validator for Node.js:

const { body, param, query, validationResult } = require('express-validator');

app.post('/api/users',
  // Validate request body
  [
    body('username')
      .isLength({ min: 3, max: 50 }).withMessage('Username must be 3-50 characters')
      .isAlphanumeric().withMessage('Username must contain only letters and numbers'),
    
    body('email')
      .isEmail().withMessage('Must provide a valid email')
      .normalizeEmail(),
    
    body('role')
      .isIn(['admin', 'user', 'manager']).withMessage('Invalid role specified')
      .optional(),
    
    body('settings.*').optional() // Allow any settings object structure
  ],
  (req, res) => {
    // Check for validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    // Process valid input
    createUser(req.body);
    res.status(201).json({ message: 'User created successfully' });
  }
);

app.get('/api/products',
  // Validate query parameters
  [
    query('page')
      .optional()
      .isInt({ min: 1 }).withMessage('Page must be a positive integer')
      .toInt(),
    
    query('limit')
      .optional()
      .isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1-100')
      .toInt(),
    
    query('sort')
      .optional()
      .isIn(['name', 'price', 'date']).withMessage('Invalid sort field')
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    // Use validated and sanitized query params
    const { page = 1, limit = 20, sort = 'date' } = req.query;
    const products = getProducts(page, limit, sort);
    
    res.json({ products });
  }
);

app.get('/api/products/:id',
  // Validate URL parameters
  param('id').isInt().withMessage('Product ID must be an integer'),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    const product = getProductById(req.params.id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    
    res.json({ product });
  }
);

API Data Filtering and Sanitization

// Sanitize and filter sensitive data
app.get('/api/users/:id', authenticateToken, async (req, res) => {
  const user = await getUserById(req.params.id);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  // Filter out sensitive data based on requesting user's role
  const isAdmin = req.user.role === 'admin';
  const isSameUser = req.user.id === user.id;
  
  // Create sanitized response
  const sanitizedUser = {
    id: user.id,
    username: user.username,
    email: isSameUser || isAdmin ? user.email : undefined,
    role: isAdmin ? user.role : undefined,
    createdAt: user.createdAt,
    // Only include personal details if admin or same user
    ...(isSameUser || isAdmin ? {
      personalDetails: user.personalDetails
    } : {})
  };
  
  res.json({ user: sanitizedUser });
});

Rate Limiting and Resource Protection

API Rate Limiting

Using Express Rate Limit:

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

// Global rate limiter - applies to all endpoints
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
  message: {
    error: 'Too many requests, please try again later.'
  }
});

// Apply global rate limiting
app.use(globalLimiter);

// Create specific limiters for sensitive endpoints
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 failed attempts per hour
  standardHeaders: true,
  skipSuccessfulRequests: true, // Only count failed requests
  message: {
    error: 'Too many failed login attempts, please try again later.'
  }
});

// Apply to login endpoint
app.post('/api/login', authLimiter, loginHandler);

// Create tier-based rate limits
const freeTierLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour window
  max: 100, // 100 requests per hour
  keyGenerator: (req) => req.user.id, // Rate limit by user ID, not IP
  standardHeaders: true,
  message: {
    error: 'Rate limit exceeded for free tier.'
  }
});

const premiumTierLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour window
  max: 1000, // 1000 requests per hour
  keyGenerator: (req) => req.user.id,
  standardHeaders: true,
  message: {
    error: 'Rate limit exceeded for premium tier.'
  }
});

// Dynamic rate limiting based on user tier
app.use('/api/data', authenticateToken, (req, res, next) => {
  if (req.user.tier === 'premium') {
    premiumTierLimiter(req, res, next);
  } else {
    freeTierLimiter(req, res, next);
  }
});

Payload Size Limits

// Limit JSON body size
app.use(express.json({ limit: '10kb' }));

// Limit URL-encoded data
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// Custom file upload limits per route
const multer = require('multer');
const upload = multer({
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});

app.post('/api/upload/avatar', 
  authenticateToken, 
  upload.single('avatar'),
  (req, res) => {
    // Handle avatar upload
  }
);

app.post('/api/upload/document', 
  authenticateToken, 
  upload.single('document'),
  (req, res) => {
    // Handle document upload
  }
);

Secure Response Handling

HTTP Security Headers

const helmet = require('helmet');

// Apply security headers to all routes
app.use(helmet());

// Configure specific headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "trusted-scripts.com"],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: []
    }
  },
  crossOriginResourcePolicy: { policy: "same-site" },
  crossOriginOpenerPolicy: { policy: "same-origin" }
}));

// Add custom security headers
app.use((req, res, next) => {
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  next();
});

CORS Configuration

const cors = require('cors');

// Basic CORS setup
const corsOptions = {
  origin: 'https://yourapplication.com', // Restrict to your domain
  methods: ['GET', 'POST', 'PUT', 'DELETE'], // Allowed methods
  allowedHeaders: ['Content-Type', 'Authorization'], // Allowed headers
  exposedHeaders: ['Content-Range', 'X-Total-Count'], // Expose these headers
  credentials: true, // Allow cookies
  maxAge: 86400 // Cache preflight requests for 24 hours
};

app.use(cors(corsOptions));

// Dynamic CORS based on environment
if (process.env.NODE_ENV === 'development') {
  app.use(cors({
    origin: '*', // Allow any origin in development
    credentials: true
  }));
} else {
  app.use(cors({
    origin: [
      'https://production-app.com',
      'https://admin.production-app.com'
    ],
    credentials: true
  }));
}

// Route-specific CORS
app.options('/api/special-endpoint', cors({
  origin: 'https://partner-site.com',
  methods: ['POST'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Special-Header']
}));

app.post('/api/special-endpoint', 
  cors({
    origin: 'https://partner-site.com',
    methods: ['POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Special-Header']
  }),
  (req, res) => {
    // Handle special endpoint
  }
);

API Logging and Monitoring

Secure API Logging

const winston = require('winston');
const expressWinston = require('express-winston');

// Create sanitized transport
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' })
  ]
});

// Sensitive data patterns to redact
const sensitiveFields = ['password', 'token', 'secret', 'authorization', 'cookie'];

// Custom sanitization function
const sanitizeRequest = (req) => {
  const sanitized = {
    headers: { ...req.headers },
    body: { ...req.body },
    query: { ...req.query }
  };
  
  // Sanitize headers
  sensitiveFields.forEach(field => {
    if (sanitized.headers[field]) {
      sanitized.headers[field] = '[REDACTED]';
    }
    
    // Case insensitive check for Authorization header
    if (field === 'authorization' && sanitized.headers['Authorization']) {
      sanitized.headers['Authorization'] = '[REDACTED]';
    }
  });
  
  // Sanitize body
  sensitiveFields.forEach(field => {
    if (sanitized.body && sanitized.body[field]) {
      sanitized.body[field] = '[REDACTED]';
    }
  });
  
  return sanitized;
};

// HTTP request logger
app.use(expressWinston.logger({
  winstonInstance: logger,
  meta: true,
  // Sanitize request data before logging
  dynamicMeta: (req, res) => {
    return {
      request: sanitizeRequest(req),
      userId: req.user ? req.user.id : 'unauthenticated',
      ip: req.ip
    };
  },
  msg: 'HTTP {{req.method}} {{req.url}}',
  expressFormat: true,
  colorize: false
}));

// Error logger
app.use(expressWinston.errorLogger({
  winstonInstance: logger,
  blacklistedMetaFields: ['exception', 'process', 'os']
}));

API Monitoring and Alerting

// Using Prometheus for API monitoring
const prometheus = require('prom-client');
const register = new prometheus.Registry();

// Add default metrics
prometheus.collectDefaultMetrics({ register });

// Create custom metrics
const httpRequestDurationMicroseconds = new prometheus.Histogram({
  name: 'http_request_duration_ms',
  help: 'Duration of HTTP requests in ms',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [10, 50, 100, 500, 1000, 5000]
});

const httpRequestTotal = new prometheus.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code']
});

const apiErrorTotal = new prometheus.Counter({
  name: 'api_errors_total',
  help: 'Total number of API errors',
  labelNames: ['method', 'route', 'error_code']
});

// Register custom metrics
register.registerMetric(httpRequestDurationMicroseconds);
register.registerMetric(httpRequestTotal);
register.registerMetric(apiErrorTotal);

// Middleware to record metrics
app.use((req, res, next) => {
  const start = Date.now();
  
  // Record when request ends
  res.on('finish', () => {
    const duration = Date.now() - start;
    const route = req.route ? req.route.path : req.path;
    
    // Record request duration
    httpRequestDurationMicroseconds
      .labels(req.method, route, res.statusCode)
      .observe(duration);
    
    // Count request
    httpRequestTotal
      .labels(req.method, route, res.statusCode)
      .inc();
    
    // Count errors
    if (res.statusCode >= 400) {
      apiErrorTotal
        .labels(req.method, route, res.statusCode)
        .inc();
    }
  });
  
  next();
});

// Expose metrics endpoint for Prometheus scraping
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

API Versioning and Deprecation

// API versioning
const apiV1Router = express.Router();
const apiV2Router = express.Router();

// Set up v1 endpoints
apiV1Router.get('/users', (req, res) => {
  // V1 implementation
  res.json({ users: getUsersV1() });
});

// Set up v2 endpoints
apiV2Router.get('/users', (req, res) => {
  // V2 implementation with enhanced data
  res.json({ 
    users: getUsersV2(),
    pagination: {
      totalCount: 100,
      page: 1,
      pageSize: 20
    }
  });
});

// Mount versioned routers
app.use('/api/v1', apiV1Router);
app.use('/api/v2', apiV2Router);

// Deprecation warning middleware for v1
app.use('/api/v1', (req, res, next) => {
  res.setHeader('Warning', '299 - "Deprecated API version. Please migrate to v2 by July 2024"');
  res.setHeader('X-API-Deprecated', 'true');
  res.setHeader('X-API-Deprecation-Date', 'July 1, 2024');
  res.setHeader('X-API-Migration-URL', 'https://api.example.com/docs/v2-migration');
  next();
});

// Default to latest version
app.use('/api', apiV2Router);

GraphQL API Security

const { ApolloServer } = require('apollo-server-express');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { applyMiddleware } = require('graphql-middleware');
const { rule, shield, allow, deny } = require('graphql-shield');

// Define GraphQL schema
const typeDefs = `
  type User {
    id: ID!
    username: String!
    email: String!
    role: String!
    sensitiveData: String
  }
  
  type Query {
    me: User
    user(id: ID!): User
    users: [User]
  }
`;

// Define resolvers
const resolvers = {
  Query: {
    me: (_, __, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      return context.user;
    },
    user: (_, { id }, context) => {
      return getUserById(id);
    },
    users: (_, __, context) => {
      return getAllUsers();
    }
  }
};

// Define permission rules
const isAuthenticated = rule({ cache: 'contextual' })(
  async (_, __, context) => {
    return !!context.user;
  }
);

const isAdmin = rule({ cache: 'contextual' })(
  async (_, __, context) => {
    return context.user && context.user.role === 'admin';
  }
);

const isSameUserOrAdmin = rule({ cache: 'contextual' })(
  async (_, args, context) => {
    if (!context.user) return false;
    if (context.user.role === 'admin') return true;
    return context.user.id === args.id;
  }
);

// Define permissions
const permissions = shield({
  Query: {
    me: isAuthenticated,
    user: isSameUserOrAdmin,
    users: isAdmin
  },
  User: {
    email: isAuthenticated,
    sensitiveData: isSameUserOrAdmin
  }
});

// Create executable schema with permissions
const schema = makeExecutableSchema({ typeDefs, resolvers });
const schemaWithPermissions = applyMiddleware(schema, permissions);

// Configure Apollo Server
const server = new ApolloServer({
  schema: schemaWithPermissions,
  context: ({ req }) => {
    // Authentication logic here
    const user = authenticateRequest(req);
    return { user };
  },
  // Set limits
  validationRules: [
    depthLimit(5), // Limit query depth
    queryComplexity({
      maximumComplexity: 1000,
      variables: {},
      onComplete: (complexity) => {
        console.log('Query complexity:', complexity);
      }
    })
  ],
  // Apollo Studio settings
  introspection: process.env.NODE_ENV !== 'production',
  playground: process.env.NODE_ENV !== 'production'
});

// Apply Apollo middleware to Express
server.applyMiddleware({ app });

Additional Resources

Clone this wiki locally