Set Up a Secure REST API

Implement JWT authentication, rate limiting, CORS policies, and UUID key generation to build a production-ready API with strong security fundamentals. 4 steps, 30 minutes.

30 minutes 4 stepsSecurity Critical

Key Challenge

Most developers launch APIs without rate limiting, CORS policies, or proper authentication. This leads to: account takeovers (weak auth), data breaches (no rate limits preventing scraping), and DDoS (unsecured endpoints).

1

Design JWT token structure and expiration

JWT (JSON Web Token) is the standard for stateless authentication. Structure: header.payload.signature. Header contains algorithm (RS256 recommended, HS256 acceptable but less secure). Payload includes: userId (unique identifier), role (admin, user, guest), issuedAt (epoch timestamp), expiresAt (epoch + duration). Example: userId='abc123', role='user', issued=1609459200, expires=1609462800 (1-hour expiration). RS256 (asymmetric) uses public-private key pairs: server signs with private key, clients verify with public key. This prevents token tampering. HS256 (symmetric) uses shared secret: both client and server have same key, faster but less secure if key is compromised. Access tokens: short-lived (15 minutes to 1 hour) for API authentication. Refresh tokens: long-lived (7–30 days) stored securely (HttpOnly cookie, not localStorage) to issue new access tokens without re-authentication. Implementation: (1) Generate RS256 key pair once during setup. (2) On login, issue access token (1-hour validity) + refresh token (7-day HttpOnly cookie). (3) Client includes access token in Authorization header: Bearer {token}. (4) On token expiry, client uses refresh token to get new access token. (5) On logout, invalidate refresh token (add to blacklist or remove from DB). Common mistake: single long-lived tokens (e.g., 30-day JWT) are risky if leaked; short-lived + refresh tokens are safer.

💡 Pro Tip: Store refresh tokens in HttpOnly cookies (immune to XSS), never in localStorage. For mobile apps, use secure device storage (Keychain for iOS, Keystore for Android). Rotate refresh token secret every 30–90 days; old tokens automatically expire.

Open JWT Generator
2

Configure rate limiting: requests per IP per minute

Rate limiting protects your API from abuse (DDoS, brute force, scraping). Standard approach: track request count per IP/user, return 429 Too Many Requests when limit exceeded. Example limits: (1) Public endpoints (signup, login): 10 requests/minute per IP (prevents brute force password guessing). (2) Authenticated endpoints: 100 requests/minute per user ID (prevents scraping). (3) Premium users: 1,000 requests/minute. (4) Admin: unlimited or very high (10,000/minute). Implementation: (1) Use Redis to store request counts (keys=IP:timestamp, values=count). (2) On each request, increment counter for IP:minute. (3) Set counter expiry to 61 seconds (survives 1-minute window plus 1 second buffer). (4) Return 429 if count > limit. Example flow: User hits /api/login from IP 192.168.1.1. Redis key='192.168.1.1:minute:2024010112'. Count=1. Second request: count=2. 11th request: return 429, try again in 59 seconds. Common mistakes: (1) Rate limiting without distinguishing authenticated users (one user DoS's everyone). (2) Counting per-endpoint globally (one popular endpoint gets hit, others throttled). (3) No delay between retries (attackers retry instantly; implement exponential backoff).

💡 Pro Tip: Implement distributed rate limiting for multi-server setups. Redis or similar shared store tracks counts globally. For local development, in-memory storage (Map, dict) works. Use libraries: express-rate-limit (Node.js), flask-limiter (Python), or cloud providers (AWS API Gateway, Cloudflare).

Open Rate Limit Config
3

Set up CORS policies to control cross-origin requests

CORS (Cross-Origin Resource Sharing) defines which external websites can call your API. Without CORS, browsers block requests from domain A trying to fetch from domain B (same-origin policy). You must explicitly allow cross-origin requests. CORS headers: (1) Access-Control-Allow-Origin: specifies allowed origins. (2) Access-Control-Allow-Methods: allowed HTTP methods (GET, POST, PUT, DELETE). (3) Access-Control-Allow-Headers: allowed request headers (Authorization, Content-Type). (4) Access-Control-Allow-Credentials: allows cookies/credentials. Example: Frontend at api.example.com wants to call backend at backend.example.com. Backend sets: Access-Control-Allow-Origin: https://api.example.com (exact domain, not wildcard). For development (localhost:3000, localhost:3001, localhost:5000 all running locally): set Access-Control-Allow-Origin: http://localhost:3000 during dev, change to production domain on deploy. CRITICAL: Never use Access-Control-Allow-Origin: * in production (allows ANY website to call your API). Wildcard is acceptable only for public read-only endpoints (news API, weather API). For user data, payment, or private endpoints, list exact domains. Preflight requests: browsers send OPTIONS request first for complex requests (non-GET, with headers). Server must respond with CORS headers. Libraries handle this automatically (express, fastify, Flask all have CORS middleware).

💡 Pro Tip: Use allowlist for CORS origins. Environment-specific: development allows localhost:*, staging allows staging.example.com, production allows example.com only. Avoid hardcoding; use environment variables.

Open CORS Generator
4

Generate UUID keys for database records and API requests

UUIDs (Universally Unique Identifiers) are 128-bit numbers globally unique, non-sequential, and hard to guess. UUID v4 is random and recommended for most cases. Example: 12a3b4cd-ef56-4789-ab01-234567890abc. Use UUIDs for: (1) Database primary keys (users table: id=UUID). (2) Resource identifiers in API responses (/api/users/{id}). (3) Idempotency keys (prevent duplicate payments/transactions). Benefits vs auto-increment (1, 2, 3, ...): (1) Non-sequential: don't reveal user count (you can't infer 'if user 5000 exists'). (2) Globally unique: safe to merge databases or migrate shards. (3) Harder to guess: attackers can't brute-force user IDs. Downside: larger storage (36 chars vs 10 chars for integer ID). For API keys and tokens, use prefix-based UUIDs for easy identification: api_12a3b4cdef564789ab012345, user_12a3b4cdef564789ab012345, token_12a3b4cdef564789ab012345. Generate in application code: most languages have UUID libraries (uuid in Python, uuid in Node.js, UUID in Java). Never generate UUIDs on client-side for sensitive data (user IDs); always server-side. Benchmark UUIDs vs integers: v4 UUID generation is fast (<1µs), negligible performance impact for most applications. Storage: use UUID type in Postgres/MySQL instead of string (more efficient).

💡 Pro Tip: Store UUIDs as BINARY(16) or UUID type in databases, not VARCHAR(36). This saves 50% storage. Use v4 for random (most common), v5 for deterministic (same input = same UUID, useful for idempotency).

Open UUID Generator

What You'll Have

JWT token structure designed with RS256 signing, access token (1-hour) and refresh token (7-day) strategy

Rate limiting configured: public endpoints (10 req/min), authenticated (100 req/min), premium (1,000 req/min)

CORS policies defined with allowlist of specific origins, no wildcard in production

UUID v4 keys generated for database records and API identifiers with prefix naming convention

Production-ready API security checklist (HTTPS, secure storage, monitoring)

Tools in this workflow

Follow this workflow in sequence to move from question to decision without losing context.

Why This Workflow Works

These four components form the minimum viable security for any public API. JWT + refresh tokens prevent account takeovers. Rate limiting prevents scraping and DDoS. CORS prevents malicious websites from exploiting user browsers. UUIDs prevent enumeration attacks where attackers guess resource IDs. Together, they create a security baseline that protects against 90% of common attacks. This workflow takes 30 minutes but saves weeks of incident response if a breach occurs.

FAQs

Should I use JWT or sessions (server-side tokens)?

JWT (stateless): no server storage, scales easily, good for microservices and mobile. Sessions (stateful): server stores session data, requires shared state across servers (Redis/database), more control but scaling harder. For most modern APIs: JWT. For traditional web apps: sessions are simpler. Hybrid: JWT for access, refresh token stored in session (best of both). JWT trade-off: larger token size (claims stored in token), but no server queries for validation. Sessions: tiny token (just ID), but require server lookup on each request.

What if an API key is leaked? How do I revoke it?

Option 1 (Stateless): Include expiry in token. Leak discovered? Users get new token within 1 hour. Downside: leaked key works until expiry. Option 2 (Whitelist): Maintain server-side list of valid tokens. On leak, remove from list immediately. Revocation is instant. Downside: requires server lookup, less scalable. Option 3 (Blacklist): Maintain list of revoked tokens. On leak, add to blacklist. Downside: blacklist grows; old tokens never leave list unless cleaned up. Recommended: short-lived JWT (1 hour) + optional blacklist for immediate revocation if needed. For permanent keys (API keys, not JWTs): use stateful storage; revoke means remove from database immediately.

How do I securely store API keys on client-side?

Never store in localStorage (vulnerable to XSS). Options: (1) HttpOnly cookies: set on login, automatically sent with requests, immune to XSS. (2) Session storage: cleared on tab close, somewhat safer than localStorage. (3) IndexedDB with encryption: encrypted on disk, requires encryption key (where to store key?). (4) Memory only: cleared on page refresh, requires re-authentication. Best practice: access token in memory (API calls use it), refresh token in HttpOnly cookie. On page refresh, silent re-authentication using refresh token. User never sees token.

Do I need rate limiting if I have authentication?

Yes. Even authenticated users can abuse API (buggy code, malicious scripts, scraping). Rate limit per user ID (not per IP). Example: User 123 tries to call /api/export 1,000 times/minute to scrape data. Rate limit: 100 req/min per user ID. User 123 gets throttled. Other users unaffected. Use different limits: basic tier (100 req/min), pro tier (1,000 req/min), based on subscription.

How often should I rotate JWT signing keys?

For access tokens (short-lived, 1 hour): monthly rotation sufficient. For refresh tokens (7-30 days): rotate keys every 90 days. Key rotation strategy: (1) Generate new key pair. (2) Sign new tokens with new key. (3) Keep old key in rotation for 24-48 hours to verify old tokens. (4) Clients refresh tokens during grace period. (5) Retire old key. This prevents service disruption while improving security. Tools: auth0, auth2go handle rotation automatically.