Security15 min read991 words

API Security Best Practices 2026: Protecting Your Endpoints

Comprehensive guide to API security. Learn authentication, authorization, rate limiting, input validation, and defense strategies for modern APIs.

JW

James Wilson

APIs are the primary attack surface for modern applications. Securing them requires a defense-in-depth approach covering authentication, authorization, input validation, and monitoring. This guide covers practical security implementations for 2026.

Authentication Best Practices

typescript
// Secure JWT implementation
import { SignJWT, jwtVerify, JWTPayload } from 'jose';
import { createHash, randomBytes } from 'crypto';

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';

interface TokenPayload extends JWTPayload {
  sub: string;
  email: string;
  roles: string[];
  tokenVersion: number;
}

async function generateAccessToken(user: User): Promise<string> {
  return new SignJWT({
    sub: user.id,
    email: user.email,
    roles: user.roles,
    tokenVersion: user.tokenVersion,
  })
    .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
    .setIssuedAt()
    .setExpirationTime(ACCESS_TOKEN_EXPIRY)
    .setIssuer('your-app')
    .setAudience('your-api')
    .sign(JWT_SECRET);
}

async function generateRefreshToken(userId: string): Promise<string> {
  const token = randomBytes(32).toString('hex');
  const hash = createHash('sha256').update(token).digest('hex');
  
  // Store hash in database with expiry
  await db.refreshToken.create({
    data: {
      userId,
      tokenHash: hash,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });
  
  return token;
}

async function verifyAccessToken(token: string): Promise<TokenPayload> {
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET, {
      issuer: 'your-app',
      audience: 'your-api',
    });
    
    // Verify token version hasn't been revoked
    const user = await db.user.findUnique({ where: { id: payload.sub } });
    if (!user || user.tokenVersion !== payload.tokenVersion) {
      throw new Error('Token revoked');
    }
    
    return payload as TokenPayload;
  } catch (error) {
    throw new Error('Invalid token');
  }
}

// Revoke all tokens for a user
async function revokeAllTokens(userId: string): Promise<void> {
  await db.user.update({
    where: { id: userId },
    data: { tokenVersion: { increment: 1 } },
  });
  
  await db.refreshToken.deleteMany({ where: { userId } });
}

Rate Limiting

typescript
// Advanced rate limiting with sliding window
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

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

const rateLimitConfigs: Record<string, RateLimitConfig> = {
  default: { windowMs: 60000, maxRequests: 100, keyPrefix: 'rl:default' },
  auth: { windowMs: 300000, maxRequests: 10, keyPrefix: 'rl:auth' },
  sensitive: { windowMs: 60000, maxRequests: 20, keyPrefix: 'rl:sensitive' },
};

async function checkRateLimit(
  identifier: string,
  configName: string = 'default'
): Promise<{ allowed: boolean; remaining: number; resetIn: number }> {
  const config = rateLimitConfigs[configName];
  const key = `${config.keyPrefix}:${identifier}`;
  const now = Date.now();
  const windowStart = now - config.windowMs;

  // Sliding window using sorted set
  const pipeline = redis.pipeline();
  
  // Remove old entries
  pipeline.zremrangebyscore(key, 0, windowStart);
  
  // Count current requests
  pipeline.zcard(key);
  
  // Add current request
  pipeline.zadd(key, now, `${now}-${Math.random()}`);
  
  // Set TTL
  pipeline.pexpire(key, config.windowMs);
  
  const results = await pipeline.exec();
  const currentCount = results![1][1] as number;
  
  if (currentCount >= config.maxRequests) {
    // Get oldest entry to calculate reset time
    const oldest = await redis.zrange(key, 0, 0, 'WITHSCORES');
    const resetIn = oldest.length > 1 
      ? parseInt(oldest[1]) + config.windowMs - now 
      : config.windowMs;
    
    return {
      allowed: false,
      remaining: 0,
      resetIn,
    };
  }

  return {
    allowed: true,
    remaining: config.maxRequests - currentCount - 1,
    resetIn: config.windowMs,
  };
}

// Middleware
export async function rateLimitMiddleware(
  req: Request,
  configName: string = 'default'
) {
  const identifier = req.headers.get('x-user-id') || 
                     req.headers.get('x-forwarded-for') || 
                     'anonymous';
  
  const result = await checkRateLimit(identifier, configName);
  
  const headers = {
    'X-RateLimit-Remaining': result.remaining.toString(),
    'X-RateLimit-Reset': Math.ceil(result.resetIn / 1000).toString(),
  };

  if (!result.allowed) {
    return new Response(
      JSON.stringify({ error: 'Too many requests' }),
      { status: 429, headers }
    );
  }

  return { headers };
}

Input Validation

typescript
// Comprehensive input validation with Zod
import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';

// Sanitization helpers
const sanitizeString = (str: string): string => {
  return DOMPurify.sanitize(str.trim(), { ALLOWED_TAGS: [] });
};

const sanitizeHtml = (html: string): string => {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href'],
  });
};

// Custom Zod schemas
const safeString = z.string().transform(sanitizeString);
const safeHtml = z.string().transform(sanitizeHtml);

const emailSchema = z
  .string()
  .email()
  .max(254)
  .toLowerCase()
  .transform(sanitizeString);

const passwordSchema = z
  .string()
  .min(12, 'Password must be at least 12 characters')
  .regex(/[a-z]/, 'Must contain lowercase letter')
  .regex(/[A-Z]/, 'Must contain uppercase letter')
  .regex(/[0-9]/, 'Must contain number')
  .regex(/[^a-zA-Z0-9]/, 'Must contain special character');

const uuidSchema = z.string().uuid();

// API request schemas
export const createUserSchema = z.object({
  email: emailSchema,
  password: passwordSchema,
  name: safeString.min(2).max(100),
  bio: safeHtml.max(1000).optional(),
});

export const updateProfileSchema = z.object({
  name: safeString.min(2).max(100).optional(),
  bio: safeHtml.max(1000).optional(),
  avatar: z.string().url().optional(),
}).refine(
  (data) => Object.keys(data).length > 0,
  { message: 'At least one field must be provided' }
);

// Query parameter validation
export const paginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
  order: z.enum(['asc', 'desc']).default('desc'),
});

// Middleware
export function validateBody<T>(schema: z.ZodSchema<T>) {
  return async (req: Request): Promise<T> => {
    const body = await req.json();
    const result = schema.safeParse(body);
    
    if (!result.success) {
      throw new ValidationError(result.error.flatten());
    }
    
    return result.data;
  };
}

Security Headers

typescript
// Security headers middleware
export function securityHeaders(response: Response): Response {
  const headers = new Headers(response.headers);
  
  // Prevent clickjacking
  headers.set('X-Frame-Options', 'DENY');
  
  // Prevent MIME sniffing
  headers.set('X-Content-Type-Options', 'nosniff');
  
  // XSS protection (legacy, but still useful)
  headers.set('X-XSS-Protection', '1; mode=block');
  
  // Referrer policy
  headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Content Security Policy
  headers.set('Content-Security-Policy', [
    "default-src 'self'",
    "script-src 'self'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self' https://api.example.com",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
  ].join('; '));
  
  // Strict Transport Security
  headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
  
  // Permissions Policy
  headers.set('Permissions-Policy', [
    'camera=()',
    'microphone=()',
    'geolocation=()',
    'interest-cohort=()',
  ].join(', '));
  
  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers,
  });
}

OWASP API Security Checklist

API Security Checklist

Authentication:

- Use strong, rotating secrets for JWT

- Implement token refresh with rotation

- Add brute force protection

- Support MFA for sensitive operations

Authorization:

- Implement RBAC or ABAC

- Validate permissions on every request

- Use principle of least privilege

Input:

- Validate all input with strict schemas

- Sanitize HTML to prevent XSS

- Use parameterized queries

Rate Limiting:

- Implement per-user and per-IP limits

- Use sliding window algorithm

- Return proper 429 responses

Monitoring:

- Log all authentication events

- Alert on suspicious patterns

- Implement security audit trails

Conclusion

API security requires multiple layers of defense. Implement strong authentication, validate all input, rate limit aggressively, and monitor continuously. These practices form the foundation of a secure API.

Need a security audit for your APIs? Contact Jishu Labs for expert security consulting and penetration testing.

JW

About James Wilson

James Wilson is a Security Architect at Jishu Labs specializing in application security and secure development practices.

Related Articles

Ready to Build Your Next Project?

Let's discuss how our expert team can help bring your vision to life.

Top-Rated
Software Development
Company

Ready to Get Started?

Get consistent results. Collaborate in real-time.
Build Intelligent Apps. Work with Jishu Labs.

SCHEDULE MY CALL