Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Home About
Products
Services
Hire
Industries
Consulting
Partners
Articles Careers Contact
Cybersecurity

API Security Best Practices: Protecting Your Endpoints

Your API is your attack surface. Every public endpoint is a door — and attackers are rattling every handle. Here's how to lock them down with authentication, rate limiting, validation, and defense-in-depth patterns that actually work.

🔒 Cybersecurity January 31, 2026 12 min read

In This Guide

APIs account for over 83% of internet traffic (Akamai, 2024), and the OWASP API Security Top 10 reads like a playbook for how breaches actually happen — broken object-level authorization, mass assignment, unrestricted resource consumption. We've audited APIs that had proper frontend authentication but zero server-side checks. An attacker doesn't use your UI. They use curl.

This guide covers the patterns we implement at Pillai Infotech for every API we build — from authentication architecture to rate limiting strategies, with real code you can adapt.

1. The API Threat Landscape — What's Actually Getting Exploited

The OWASP API Security Top 10 (2023) is the baseline. Here's what each vulnerability looks like in practice:

# Vulnerability Real-World Attack Defense
API1 BOLA Change /users/123/orders to /users/456/orders → see someone else's orders Check ownership on every object access
API2 Broken Auth Credential stuffing against /login with no rate limit MFA, rate limiting, account lockout
API3 Object Property Level API returns is_admin, ssn, internal fields in response Explicit response schemas, never return full DB rows
API4 Unrestricted Consumption GET /search?q=*&limit=999999 → server OOM Max page size, rate limits, resource quotas
API5 Broken Function Level Auth Regular user calls DELETE /admin/users/123 Role-based middleware on every route
API6 Server-Side Request Forgery {"url": "http://169.254.169.254/metadata"} → AWS keys leaked URL allowlist, block internal IPs, IMDSv2
API7 Security Misconfiguration Debug mode on, stack traces in production, permissive CORS Environment-specific configs, security headers
API8 Lack of Protection from Automated Threats Bots scraping /products, price comparison services hammering API Bot detection, CAPTCHA, behavioral analysis
API9 Improper Inventory Mgmt Old /api/v1/ still live with no auth while v3 is secured API versioning policy, sunset old versions
API10 Unsafe Consumption of APIs Trusting third-party API response without validation → stored XSS Validate and sanitize all external API responses

Notice: 6 of 10 are authorization and access control issues, not encryption or cryptography. API security is fundamentally about who can access what, not about what cipher suite you're using.

2. Authentication — API Keys, OAuth 2.0, JWTs Done Right

Different authentication mechanisms serve different purposes. Here's when to use each:

Method Use Case Pros Cons
API Keys Server-to-server, internal services Simple, fast, easy to rotate No user identity, easy to leak
OAuth 2.0 + PKCE User-facing apps, third-party integrations Delegated auth, scoped access, standard Complex flow, token management
JWT (Bearer) Stateless microservice auth No session store, carries claims Can't revoke instantly, size in headers
mTLS Zero-trust service mesh, high-security internal Strongest machine identity Certificate management overhead

JWT Implementation — The Right Way

We see the same JWT mistakes repeatedly. Here's what a secure implementation looks like:

// Node.js — Secure JWT setup
import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

// ✅ Use RS256 (asymmetric) for multi-service architectures
// ✅ Use HS256 (symmetric) only for single-service
const ACCESS_TOKEN_EXPIRY = '15m';   // Short-lived
const REFRESH_TOKEN_EXPIRY = '7d';   // Longer, stored securely

function generateTokenPair(user) {
  const accessToken = jwt.sign(
    {
      sub: user.id,
      email: user.email,
      roles: user.roles,      // Only include what consumers need
      // ❌ Never: password, ssn, full profile
    },
    process.env.JWT_SECRET,
    {
      algorithm: 'HS256',
      expiresIn: ACCESS_TOKEN_EXPIRY,
      issuer: 'api.pillaiinfotech.com',
      audience: 'pillaiinfotech.com',
    }
  );

  const refreshToken = randomBytes(40).toString('hex');
  // Store refresh token in DB — enables revocation
  storeRefreshToken(user.id, refreshToken, '7d');

  return { accessToken, refreshToken };
}

// Middleware — verify on every request
function authenticate(req, res, next) {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  try {
    const decoded = jwt.verify(header.slice(7), process.env.JWT_SECRET, {
      algorithms: ['HS256'],       // ✅ Whitelist algorithms
      issuer: 'api.pillaiinfotech.com',
      audience: 'pillaiinfotech.com',
    });
    req.user = decoded;
    next();
  } catch (err) {
    // ❌ Don't tell attacker WHY it failed
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Common JWT mistakes we catch in security audits:

3. Authorization — BOLA Is the #1 API Vulnerability

Broken Object Level Authorization (BOLA) is the most common API vulnerability because it's easy to miss. The API authenticates the user (confirms who they are) but doesn't check if they should access that specific resource.

// Express.js — BOLA vulnerability vs fix
// ❌ VULNERABLE — any authenticated user can access any order
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await db.query('SELECT * FROM orders WHERE id = ?',
    [req.params.orderId]);
  return res.json(order);
});

// ✅ FIXED — always filter by the authenticated user
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await db.query(
    'SELECT * FROM orders WHERE id = ? AND user_id = ?',
    [req.params.orderId, req.user.sub]  // ← ownership check
  );
  if (!order) return res.status(404).json({ error: 'Not found' });
  return res.json(order);
});

// For admin access to any resource — explicit role check
app.get('/api/admin/orders/:orderId', authenticate, authorize('admin'),
  async (req, res) => {
    const order = await db.query('SELECT * FROM orders WHERE id = ?',
      [req.params.orderId]);
    return res.json(order);
  }
);

Authorization Middleware Pattern

// Reusable authorization middleware
function authorize(...requiredRoles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }

    const hasRole = requiredRoles.some(role =>
      req.user.roles.includes(role)
    );

    if (!hasRole) {
      // Log the attempt — could indicate enumeration
      logger.warn('Authorization denied', {
        user: req.user.sub,
        required: requiredRoles,
        path: req.path,
        ip: req.ip,
      });
      return res.status(403).json({ error: 'Insufficient permissions' });
    }

    next();
  };
}
What We've Learned at Pillai Infotech

We audit every API endpoint with a "change-the-ID" test before launch. Literally: authenticate as User A, call every endpoint with User B's resource IDs. If anything returns data, it's a BOLA vulnerability. Takes 30 minutes and catches issues that automated scanners miss.

4. Input Validation and Schema Enforcement

Never trust client input. Every field, every parameter, every header. Validate at the API boundary — not deep inside your business logic.

// Zod schema validation — Node.js
import { z } from 'zod';

const CreateOrderSchema = z.object({
  product_id: z.string().uuid(),
  quantity: z.number().int().min(1).max(100),
  shipping_address: z.object({
    line1: z.string().min(1).max(200),
    line2: z.string().max(200).optional(),
    city: z.string().min(1).max(100),
    state: z.string().length(2),
    zip: z.string().regex(/^\d{5}(-\d{4})?$/),
    country: z.string().length(2),
  }),
  // ❌ Never accept: user_id, price, discount, role from client
});

app.post('/api/orders', authenticate, async (req, res) => {
  const result = CreateOrderSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.issues.map(i => ({
        field: i.path.join('.'),
        message: i.message,
      })),
    });
  }

  // result.data is typed and sanitized — safe to use
  const order = await createOrder(req.user.sub, result.data);
  return res.status(201).json(order);
});

Mass Assignment Prevention

Mass assignment happens when your API blindly passes request body to the database. This is how attackers set is_admin: true on their own account.

// PHP — Mass assignment prevention
// ❌ VULNERABLE — accepts anything the client sends
$db->execute(
    "UPDATE users SET " . buildSetClause($requestBody) . " WHERE id = ?",
    [$userId]
);

// ✅ SAFE — explicit allowlist of updatable fields
$allowed = ['name', 'email', 'phone', 'timezone'];
$updates = array_intersect_key($requestBody, array_flip($allowed));

if (empty($updates)) {
    return jsonResponse(400, ['error' => 'No valid fields to update']);
}

$setClauses = [];
$params = [];
foreach ($updates as $field => $value) {
    $setClauses[] = "$field = ?";
    $params[] = $value;
}
$params[] = $userId;

$db->execute(
    "UPDATE users SET " . implode(', ', $setClauses) . " WHERE id = ?",
    $params
);

5. Rate Limiting and Throttling

Without rate limiting, your API is an open invitation for credential stuffing, scraping, and DDoS. But not all endpoints need the same limits.

Endpoint Type Rate Limit Key By Why
Login / Auth 5 req/min IP + username Prevent credential stuffing
Password Reset 3 req/hour IP + email Prevent email bombing
API Read 100 req/min API key / user ID Prevent scraping and abuse
API Write 30 req/min User ID Prevent spam / data pollution
File Upload 5 req/min User ID Prevent storage abuse
Search 20 req/min IP + user ID Expensive queries, prevent enumeration
// Redis sliding window rate limiter
import Redis from 'ioredis';
const redis = new Redis();

async function rateLimit(key, maxRequests, windowSecs) {
  const now = Date.now();
  const windowStart = now - windowSecs * 1000;

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart); // Remove old entries
  pipeline.zadd(key, now, `${now}-${Math.random()}`); // Add current
  pipeline.zcard(key);                              // Count in window
  pipeline.expire(key, windowSecs);                 // Auto-cleanup

  const results = await pipeline.exec();
  const count = results[2][1];

  return {
    allowed: count <= maxRequests,
    remaining: Math.max(0, maxRequests - count),
    resetAt: new Date(now + windowSecs * 1000),
  };
}

// Middleware
async function rateLimitMiddleware(req, res, next) {
  const key = `rl:${req.path}:${req.user?.sub || req.ip}`;
  const { allowed, remaining, resetAt } = await rateLimit(key, 100, 60);

  // Always set headers — helps legitimate clients back off
  res.set('X-RateLimit-Limit', '100');
  res.set('X-RateLimit-Remaining', String(remaining));
  res.set('X-RateLimit-Reset', resetAt.toISOString());

  if (!allowed) {
    return res.status(429).json({
      error: 'Rate limit exceeded',
      retryAfter: Math.ceil((resetAt - Date.now()) / 1000),
    });
  }
  next();
}

Always return X-RateLimit-* headers. Good API clients (like the ones we build at Pillai Infotech) use these to implement exponential backoff automatically. Bad actors ignore them — which is fine, because the server enforces limits regardless.

6. CORS — Stop Misconfiguring It

CORS misconfigurations are in nearly every API audit we do. The most dangerous: reflecting the Origin header back as Access-Control-Allow-Origin — effectively disabling CORS entirely.

// Express.js — CORS done right
import cors from 'cors';

// ❌ DANGEROUS — allows any origin
app.use(cors({ origin: true, credentials: true }));

// ❌ DANGEROUS — reflects Origin header (same as above)
app.use(cors({
  origin: (origin, callback) => callback(null, origin),
  credentials: true,
}));

// ✅ SAFE — explicit allowlist
const ALLOWED_ORIGINS = [
  'https://pillaiinfotech.com',
  'https://app.pillaiinfotech.com',
  'https://cmdcenter.pillaiinfotech.com',
];

if (process.env.NODE_ENV === 'development') {
  ALLOWED_ORIGINS.push('http://localhost:3000');
  ALLOWED_ORIGINS.push('http://localhost:8888');
}

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (server-to-server, Postman)
    if (!origin) return callback(null, true);
    if (ALLOWED_ORIGINS.includes(origin)) {
      return callback(null, true);
    }
    callback(new Error('Blocked by CORS'));
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  maxAge: 86400,  // Cache preflight for 24h
}));

Quick CORS checklist: Never use Access-Control-Allow-Origin: * with credentials. Never reflect the Origin header. Always specify exact allowed methods and headers. Set maxAge to reduce preflight requests. And remember — CORS only protects browsers. API-to-API calls bypass CORS entirely, so it's not a replacement for authentication.

7. Logging, Monitoring, and Incident Detection

You can't protect what you can't see. Security logging needs to answer: Who did what, to which resource, when, and from where?

What to Log

Event Log Level Alert? Data to Capture
Successful login INFO No User ID, IP, user-agent, geo
Failed login WARN After 5 in 10min Username attempted, IP, reason
Authorization denied WARN Yes — immediately User, resource, required role, path
Rate limit hit WARN After 10 in 1min Key, endpoint, count, IP
Input validation failure INFO Pattern-based Field, violation, sanitized input
Admin action INFO Yes — always Admin user, action, target, before/after
Data export / bulk read INFO If >1000 records User, query, record count, filters

What to never log: passwords, tokens, API keys, credit card numbers, SSNs, or any PII that isn't needed for investigation. If you log request bodies, redact sensitive fields first.

For more on building observability into your stack, see our monitoring and observability guide.

8. API Gateway Security Patterns

An API gateway centralizes cross-cutting security concerns. Instead of implementing auth, rate limiting, and logging in every service, do it once at the edge.

Client Request │ ▼ ┌──────────────────────────────┐ │ API Gateway │ │ ┌────────────────────────┐ │ │ │ TLS Termination │ │ │ │ Rate Limiting │ │ │ │ Authentication │ │ │ │ Request Validation │ │ │ │ IP Allowlist/Blocklist│ │ │ │ Request Logging │ │ │ │ Response Sanitization │ │ │ └────────────────────────┘ │ └──────────┬───────────────────┘ │ Internal network only ┌─────┼─────┐ ▼ ▼ ▼ ┌─────┐┌─────┐┌─────┐ │Svc A││Svc B││Svc C│ ← Only accept traffic from gateway └─────┘└─────┘└─────┘
Gateway Type Best For Security Features
Kong Open source Self-hosted, plugin ecosystem JWT, OAuth, rate limiting, IP restriction, bot detection
AWS API Gateway Managed AWS-native architectures IAM auth, Lambda authorizers, WAF integration, throttling
Cloudflare API Shield Edge DDoS protection, global CDN mTLS, schema validation, anomaly detection, bot management
Nginx + Lua Self-hosted Lightweight, full control Custom rate limiting, JWT validation, header injection, IP filtering
# Nginx API gateway — security headers + rate limiting
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;

server {
    listen 443 ssl http2;
    server_name api.pillaiinfotech.com;

    # TLS 1.2+ only — no legacy protocols
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # Security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-XSS-Protection "0" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Hide server version
    server_tokens off;

    # Request size limits
    client_max_body_size 10m;
    client_body_buffer_size 128k;

    location /api/v1/auth/login {
        limit_req zone=login burst=3 nodelay;
        proxy_pass http://backend;
    }

    location /api/v1/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://backend;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Request-ID $request_id;
    }

    # Block common attack paths
    location ~ /\.(git|env|htaccess) {
        return 404;
    }
}

9. The API Security Checklist

We use this checklist before every API goes to production. Not every item applies to every API, but every item should be consciously decided on.

Category Check Priority
Authentication Every endpoint requires authentication (unless explicitly public) Critical
Tokens expire in ≤15 minutes, refresh tokens stored server-side Critical
JWT algorithm whitelisted, issuer and audience validated High
Authorization Object-level authorization on every resource endpoint Critical
Function-level authorization — admin endpoints require admin role Critical
Input Schema validation on all request bodies (Zod, Joi, JSON Schema) Critical
Query parameters validated and typed (no raw string to SQL) Critical
File uploads: type whitelist, size limit, virus scan High
Transport TLS 1.2+ everywhere, HSTS enabled, no mixed content Critical
CORS configured with explicit origin allowlist High
Rate Limiting Auth endpoints: ≤5 req/min per IP Critical
Pagination enforced with max page size (e.g., 100) High
Monitoring Security events logged with user, IP, action, resource High
Alerts on auth failures, authorization denials, rate limit spikes High
What We've Learned at Pillai Infotech

We've built this checklist into our CI pipeline. A pre-deploy script checks for common API security anti-patterns: routes without authentication middleware, direct request body to SQL (potential injection), and Access-Control-Allow-Origin: * in config files. It's not a full security audit — but it catches the "forgot to add the auth middleware" class of bugs before they reach production.

10. Frequently Asked Questions

Should I use API keys or OAuth 2.0?

API keys for server-to-server communication where both parties are under your control. OAuth 2.0 with PKCE for any user-facing application or third-party integration. Never expose API keys in frontend JavaScript — they'll end up in browser dev tools, GitHub search results, and Shodan within hours.

How do I protect against DDoS attacks on my API?

Layer your defenses: CDN/WAF at the edge (Cloudflare, AWS Shield), rate limiting at the API gateway, and resource quotas in your application. For critical APIs, add proof-of-work challenges or CAPTCHA on suspicious traffic patterns. No single layer stops all DDoS — it's about making attacks expensive relative to legitimate traffic.

Is GraphQL more secure than REST?

Neither is inherently more secure — the same auth, authorization, and validation principles apply. GraphQL adds unique attack surfaces: query complexity attacks (deeply nested queries), introspection exposure in production, and batching abuse. Disable introspection in production, enforce query depth limits (typically 10-15), and use persisted queries. See our GraphQL vs REST comparison for architectural trade-offs.

How often should I rotate API keys?

Every 90 days minimum, immediately if a key might be compromised. Design for rotation from the start: support multiple active keys per client (old key + new key), implement key expiry dates, and automate the rotation process. We've seen teams that can't rotate keys because their systems hardcode them everywhere — that's a design failure, not a security choice.

What's the best way to handle API versioning securely?

URL path versioning (/api/v2/) is simplest and most visible. The security concern is sunset: old API versions accumulate vulnerabilities while attention focuses on the latest version. Set deprecation dates, monitor old version traffic, and decommission aggressively. If /api/v1/ has a known vulnerability and 2% of traffic, shut it down and force migration.

🔒

Pillai Infotech LLP

We build secure APIs from day one — authentication architecture, input validation, rate limiting, and zero-trust security patterns. Let's secure your API.

Related Articles

Cybersecurity Best Practices for Software Development Teams → Zero Trust Security Architecture: Implementation Guide → DevSecOps: Integrating Security Into Your CI/CD Pipeline →

Pillai Infotech Engineering Team

We build production software across AI, cloud, web, and mobile — sharing real-world insights from projects delivered for startups and enterprises across India and globally.

Need Help Securing Your APIs?

From API architecture to penetration testing, our security team builds APIs that protect your business and your users' data.

Get a Security Assessment Security Consulting