API Security in 2026: 8 Essential Practices to Protect Your REST APIs from Modern Attacks

APIs are the backbone of modern applications, but they’re also the #1 attack vector in 2026. With API attacks up 300% over the past three years, securing your REST APIs isn’t optional — it’s survival. This guide walks you through eight battle-tested practices with real code examples you can implement today.

1. Always Validate and Sanitize Input

Never trust incoming data. Use schema validation libraries to enforce strict input rules at every endpoint.

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

const UserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
  age: z.number().int().min(13).max(120),
});

app.post('/api/users', (req, res) => {
  const result = UserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() });
  }
  // Safe to use result.data
  createUser(result.data);
});

Zod catches malformed data before it ever reaches your business logic, preventing injection attacks and data corruption.

2. Implement Rate Limiting with Sliding Windows

Fixed-window rate limiting has gaps. Sliding window algorithms provide smoother, harder-to-exploit throttling.

// Express rate limiting with sliding window
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();

const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 30,             // 30 requests per window
  standardHeaders: true,
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
  message: { error: 'Too many requests. Try again shortly.' },
});

app.use('/api/', apiLimiter);

Use Redis-backed stores in production so rate limits work across multiple server instances.

3. Use Short-Lived JWTs with Refresh Token Rotation

Long-lived tokens are dangerous. Keep access tokens short (5-15 minutes) and rotate refresh tokens on every use.

import jwt from 'jsonwebtoken';

function generateTokens(userId) {
  const accessToken = jwt.sign(
    { sub: userId, type: 'access' },
    process.env.JWT_SECRET,
    { expiresIn: '10m' }
  );

  const refreshToken = jwt.sign(
    { sub: userId, type: 'refresh', jti: crypto.randomUUID() },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  // Store refresh token hash in DB for rotation tracking
  storeRefreshToken(userId, refreshToken);
  return { accessToken, refreshToken };
}

async function rotateRefreshToken(oldRefreshToken) {
  const payload = jwt.verify(oldRefreshToken, process.env.REFRESH_SECRET);

  // Check if this token was already used (replay attack)
  const isValid = await validateAndInvalidateToken(payload.jti);
  if (!isValid) {
    // Token reuse detected — revoke ALL tokens for this user
    await revokeAllTokens(payload.sub);
    throw new Error('Token reuse detected');
  }

  return generateTokens(payload.sub);
}

If a refresh token is used twice, it means it was stolen. Revoke everything and force re-authentication.

4. Enforce Object-Level Authorization (BOLA Prevention)

Broken Object-Level Authorization (BOLA) remains the OWASP API #1 threat. Always verify that the authenticated user owns the resource they’re requesting.

// Middleware to check resource ownership
function authorizeResource(resourceFetcher) {
  return async (req, res, next) => {
    const resource = await resourceFetcher(req.params.id);
    if (!resource) return res.status(404).json({ error: 'Not found' });
    if (resource.ownerId !== req.user.id) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    req.resource = resource;
    next();
  };
}

// Usage
app.get('/api/orders/:id',
  authenticate,
  authorizeResource((id) => Order.findById(id)),
  (req, res) => res.json(req.resource)
);

5. Add Security Headers and CORS Lockdown

Misconfigured CORS is an open invitation. Be explicit about allowed origins — never use wildcards in production.

import helmet from 'helmet';
import cors from 'cors';

app.use(helmet());

app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400,
}));

Helmet sets critical headers like X-Content-Type-Options, Strict-Transport-Security, and X-Frame-Options automatically.

6. Log Everything, Alert on Anomalies

You can’t protect what you can’t see. Structured logging with anomaly detection catches attacks in progress.

import pino from 'pino';

const logger = pino({ level: 'info' });

function auditMiddleware(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    logger.info({
      method: req.method,
      path: req.path,
      status: res.statusCode,
      duration: Date.now() - start,
      ip: req.ip,
      userId: req.user?.id || 'anonymous',
      userAgent: req.headers['user-agent'],
    });

    // Alert on suspicious patterns
    if (res.statusCode === 401 || res.statusCode === 403) {
      trackFailedAuth(req.ip);
    }
  });
  next();
}

Pipe these logs to a SIEM tool or set up alerts for spikes in 401/403 responses from a single IP.

7. Use API Versioning to Deprecate Insecure Endpoints

Old API versions often have known vulnerabilities. Version your APIs and set sunset dates.

// Version-aware router
import { Router } from 'express';

const v1Router = Router();
const v2Router = Router();

// v1 — deprecated, returns sunset header
v1Router.use((req, res, next) => {
  res.set('Sunset', 'Sat, 01 Jul 2026 00:00:00 GMT');
  res.set('Deprecation', 'true');
  res.set('Link', '</api/v2>; rel="successor-version"');
  next();
});

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

Communicate deprecation via HTTP headers so clients can migrate before you kill old versions.

8. Encrypt Sensitive Data in Transit AND at Rest

TLS protects data in transit, but sensitive fields in your database need encryption too.

import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');

function encrypt(text) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const tag = cipher.getAuthTag().toString('hex');
  return `${iv.toString('hex')}:${tag}:${encrypted}`;
}

function decrypt(data) {
  const [ivHex, tagHex, encrypted] = data.split(':');
  const decipher = crypto.createDecipheriv(
    ALGORITHM, KEY, Buffer.from(ivHex, 'hex')
  );
  decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

Use AES-256-GCM for authenticated encryption — it prevents both reading and tampering with the data.

Wrapping Up

API security isn’t a one-time task. These eight practices form a solid baseline:

  1. Validate input with schema libraries
  2. Rate limit with sliding windows
  3. Use short-lived JWTs with refresh rotation
  4. Check object-level authorization on every request
  5. Lock down CORS and add security headers
  6. Log and monitor for anomalies
  7. Version your APIs and sunset old ones
  8. Encrypt sensitive data at rest

Start with input validation and rate limiting — they stop the most common attacks. Then layer on the rest. Your future self (and your users) will thank you.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials