Security4 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.

JL

Jishu Labs

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.

References

  1. OWASP API Security Top 10OWASP
  2. RFC 9700: Best Current Practice for OAuth 2.0 SecurityIETF
  3. REST Security Cheat SheetOWASP
JL

About Jishu Labs

Jishu Labs is a software development company founded in 2016. We build custom software, AI/ML systems, and full-stack web and mobile applications for clients, and we make eight AI tools for software teams.

Related Articles

Security3 min read

What Is Prompt Injection?

Prompt injection is when untrusted text reaching a model's context gets treated as instruction rather than data. It has held OWASP's number one LLM risk slot across every edition, and agents made it materially worse.

Jishu Labs

August 7, 2026

Security3 min read

Model Risk and Audit Trails in Financial Services AI

Financial services has governed models for decades. Generative AI does not escape that framework - it stresses it. What model risk management asks of an LLM feature, and what to instrument.

Jishu Labs

August 4, 2026

Security3 min read

Shipping AI Features Under HIPAA

Adding an LLM to a product that touches protected health information changes who your subprocessors are, what your logs contain, and what you must be able to prove. An engineering view of the constraints.

Jishu Labs

August 3, 2026

Ready to Build Your Next Project?

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

AI Tools,
Built
End-to-End

Ready to Get Started?

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

SCHEDULE MY CALL