Skip to main content
Back to blog
April 28, 2026|8 min read

REST API Security Best Practices 2026: HTTPS, JWT, Rate Limiting & CORS

REST API security is the backbone of modern web application defense. This guide covers every layer from HTTPS enforcement and JWT authentication to input validation, rate limiting, and safe error handling.

ZeriFlow Team

1,494 words

REST API Security Best Practices 2026: HTTPS, JWT, Rate Limiting & CORS

REST API security is foundational — every web and mobile application relies on APIs, and APIs are the primary attack surface in modern software. A single misconfigured endpoint can expose your entire user database. This guide covers the complete security stack for production REST APIs in 2026.

Start with a free baseline scan of your live API at ZeriFlow — it checks 80+ security controls including headers, TLS grade, and common endpoint exposures.


1. Enforce HTTPS for Every Request

All REST API traffic must use HTTPS. HTTP transmits credentials, tokens, and data in plaintext — any network observer can read and modify them.

At the web server (Nginx), redirect all HTTP to HTTPS:

nginx
server {
    listen 80;
    server_name api.yourdomain.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers   ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always;
}

Also enforce HTTPS in your application layer as a second line of defense (shown in framework-specific guides above).

Check your TLS grade with [ZeriFlow](https://zeriflow.com) — it tests for TLS 1.0/1.1 support, weak ciphers, certificate expiry, and HSTS configuration.


2. Authentication: JWT and OAuth 2.0

JWT best practices:

javascript
const jwt = require('jsonwebtoken');

// Signing — always use asymmetric keys (RS256/ES256) for production
const privateKey = process.env.JWT_PRIVATE_KEY;

function generateTokens(userId, role) {
  const accessToken = jwt.sign(
    { sub: userId, role, type: 'access' },
    privateKey,
    {
      algorithm:  'RS256',
      expiresIn:  '15m',    // Short-lived
      issuer:     'api.yourdomain.com',
      audience:   'yourdomain.com',
    }
  );

  const refreshToken = jwt.sign(
    { sub: userId, type: 'refresh' },
    privateKey,
    { algorithm: 'RS256', expiresIn: '7d' }
  );

  return { accessToken, refreshToken };
}

// Verification — always verify signature AND claims
const publicKey = process.env.JWT_PUBLIC_KEY;

function verifyAccessToken(token) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    issuer:     'api.yourdomain.com',
    audience:   'yourdomain.com',
  });
}

Token storage rules: - Access tokens: memory only (never localStorage) - Refresh tokens: httpOnly, Secure, SameSite=Strict cookies - Never log tokens, never include them in URLs

Rotate refresh tokens on every use (refresh token rotation):

javascript
app.post('/auth/refresh', async (req, res) => {
  const oldRefreshToken = req.cookies.refresh_token;
  const payload = verifyRefreshToken(oldRefreshToken);

  // Check token has not been revoked
  const stored = await RefreshToken.findOne({ token: oldRefreshToken });
  if (!stored || stored.revoked) {
    // Possible token reuse attack — revoke all tokens for this user
    await RefreshToken.revokeAllForUser(payload.sub);
    return res.status(401).json({ error: 'Invalid refresh token' });
  }

  // Revoke old, issue new
  await stored.revoke();
  const { accessToken, refreshToken } = generateTokens(payload.sub, payload.role);
  await RefreshToken.create({ token: refreshToken, userId: payload.sub });

  res.cookie('refresh_token', refreshToken, {
    httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 3600 * 1000,
  });
  res.json({ accessToken });
});

3. Rate Limiting

Without rate limiting, your API is vulnerable to brute force, credential stuffing, and enumeration attacks.

javascript
const rateLimit    = require('express-rate-limit');
const slowDown     = require('express-slow-down');

// Progressive delay before hard limit
const speedLimiter = slowDown({
  windowMs:        15 * 60 * 1000,
  delayAfter:      50,    // Start slowing down after 50 requests
  delayMs:         (used, req) => (used - 50) * 200, // Add 200ms per request over limit
});

// Hard limit
const apiLimiter = rateLimit({
  windowMs:        15 * 60 * 1000,
  max:             100,
  standardHeaders: true,
  legacyHeaders:   false,
});

// Strict limit for sensitive endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max:      5,
  skipSuccessfulRequests: true,
  handler: (req, res) => res.status(429).json({
    error: 'Too many authentication attempts'
  }),
});

app.use('/api/', speedLimiter, apiLimiter);
app.use('/api/auth/', authLimiter);
app.use('/api/password-reset/', authLimiter);

Always return the Retry-After header so clients know when to retry:

javascript
const limiter = rateLimit({
  handler: (req, res, next, options) => {
    res.status(429)
      .set('Retry-After', Math.ceil(options.windowMs / 1000))
      .json({ error: 'Rate limit exceeded', retryAfter: options.windowMs / 1000 });
  },
});

4. Input Validation and Sanitization

Never trust client-supplied data. Validate every parameter — path, query, body, and header.

javascript
const { z } = require('zod');

const createOrderSchema = z.object({
  productId: z.string().uuid(),
  quantity:  z.number().int().min(1).max(100),
  address: z.object({
    street:  z.string().max(200),
    city:    z.string().max(100),
    country: z.string().length(2).toUpperCase(), // ISO 3166 alpha-2
  }),
});

app.post('/orders', authenticate, async (req, res) => {
  const result = createOrderSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      error:   'Invalid request body',
      details: result.error.flatten().fieldErrors,
    });
  }

  const { productId, quantity, address } = result.data;
  // All fields are validated and typed
  const order = await Order.create({ userId: req.user.id, productId, quantity, address });
  res.status(201).json(order);
});

For SQL queries, always use parameterized statements regardless of your ORM:

javascript
// Dangerous
const users = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// Safe
const users = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);

5. CORS Policy

Configure CORS to allow only your known client origins.

javascript
const cors = require('cors');

const corsOptions = {
  origin: (origin, callback) => {
    const allowed = ['https://app.yourdomain.com', 'https://yourdomain.com'];
    if (!origin || allowed.includes(origin)) return callback(null, true);
    callback(new Error(`Origin ${origin} not permitted`));
  },
  methods:          ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders:   ['Content-Type', 'Authorization', 'X-Request-ID'],
  exposedHeaders:   ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'Retry-After'],
  credentials:      true,
  maxAge:           86400,
};

app.use('/api/', cors(corsOptions));

For internal APIs never accessed from browsers, disable CORS entirely by not including the header — the absence of an Access-Control-Allow-Origin header means browsers block cross-origin requests.


6. API Versioning and Safe Error Messages

Version your API to allow breaking security changes without disrupting clients:

javascript
// URL versioning (most visible, easiest to manage)
app.use('/api/v1/', v1Router);
app.use('/api/v2/', v2Router);

// Or header versioning
app.use((req, res, next) => {
  req.apiVersion = req.headers['api-version'] || '1';
  next();
});

Never leak internal details in error responses:

javascript
// Dangerous
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,    // Leaks internal details
    stack: err.stack,      // NEVER do this in production
    query: err.query,      // Leaks SQL
  });
});

// Safe
app.use((err, req, res, next) => {
  // Log full details internally
  console.error({ requestId: req.id, error: err.message, stack: err.stack });

  // Return generic message to client
  const status = err.statusCode || 500;
  const publicMessage = status < 500 ? err.message : 'An internal error occurred';

  res.status(status).json({
    error:     publicMessage,
    requestId: req.id,   // For support lookup
  });
});

For auth endpoints specifically, return identical error messages for 'user not found' and 'wrong password' — different messages enable user enumeration attacks.


FAQ

### Q: Should I use JWT or opaque tokens for REST APIs? A: JWTs are stateless and self-contained — good for microservices. Opaque tokens require a database lookup but can be revoked immediately. For most applications, use short-lived JWTs (15 min) with refresh token rotation, giving you near-immediate revocation on refresh.

### Q: How do I protect against IDOR (Insecure Direct Object Reference)? A: Never use sequential IDs in public APIs. Use UUIDs or opaque identifiers, and always verify the authenticated user owns the resource before returning or modifying it: WHERE id = $1 AND user_id = $2.

### Q: What HTTP methods should I expose on a public REST API? A: Only expose the methods you actually use. Configure your web server to return 405 for disallowed methods. Never expose TRACE (enables XST attacks) or OPTIONS beyond what CORS preflight requires.

### Q: How do I handle API authentication for mobile apps? A: Use OAuth 2.0 PKCE flow for mobile apps — it provides secure authorization without requiring a client secret to be stored on the device. Combine with certificate pinning to prevent MITM attacks.

### Q: Is API gateway-level security sufficient, or do I need application-level security too? A: Both are required. API gateways handle rate limiting, authentication, and TLS termination at the perimeter. Application-level validation, authorization, and business logic security must exist in the application itself — never rely solely on the gateway.


Conclusion

REST API security in 2026 is a stack of layers: TLS enforced at the transport layer, JWT or OAuth for authentication, rate limiting on every endpoint, input validation via schemas, strict CORS, versioned endpoints, and error responses that reveal nothing internal. Each layer independently limits the blast radius of a breach.

Verify your live API configuration with ZeriFlow's free scanner — it checks your TLS grade, security headers, CORS configuration, and common exposed endpoints in one scan, giving you a complete outside-in picture of your API's security posture.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading