How to Secure a REST API: A Practical Step-by-Step Guide
A REST API is one of the most exposed parts of any modern application: it accepts untrusted input from the public internet and often sits directly in front of your data. This guide walks through the concrete controls that protect an API, why each matters, and the mistakes that quietly defeat them.
Why API security deserves its own checklist
Web applications get most of the security attention, but APIs fail differently. There is no browser enforcing same-origin rules, no human reading an error page, and often no UI hiding which fields exist. An attacker can script thousands of requests, enumerate object IDs, and replay tokens at machine speed.
The dominant API risks are well documented in the OWASP API Security Top 10, and the top two are consistently broken object-level authorization (one user reading another user's records by changing an ID) and broken authentication. Most of the work below targets those two categories directly.
Step 1: Require HTTPS everywhere
Every endpoint must be served over TLS. Without it, bearer tokens, API keys, and request bodies travel in cleartext and can be read or modified on the network. There is no acceptable reason to expose a production API over plain HTTP.
- Redirect all HTTP traffic to HTTPS, and send an
HSTSheader (Strict-Transport-Security) so clients refuse to downgrade. - Terminate TLS at a load balancer or gateway you control, and keep certificates current and auto-renewed.
- For service-to-service calls in a zero-trust network, consider mutual TLS so both ends authenticate.
Step 2: Authenticate every request
Authentication answers "who is calling." Never roll your own scheme. Pick a proven standard and apply it to every non-public route, including ones you assume nobody knows.
Choosing an authentication approach
- API keys are simple and fine for server-to-server access, but they identify an application, not a granular user, and offer no built-in expiry. Treat a key like a password: high entropy, hashed at rest, rotatable, and revocable.
- OAuth 2.0 with bearer tokens (often JWTs) is the right fit for user-facing APIs and third-party access. It separates the authorization server from your resource server and supports scopes and short-lived tokens. For a deeper comparison of the two models, see our explainer on OAuth2 vs JWT.
If you use JWTs, validate them strictly: verify the signature, confirm the iss and aud claims, reject expired tokens via exp, and explicitly pin the expected algorithm so an attacker cannot downgrade it to none or swap the signing scheme. You can inspect a token's claims while debugging with the JWT Decoder, and assemble test tokens with the JWT Builder. Generate strong API keys and client secrets with a Password Generator rather than a predictable string.
Step 3: Enforce authorization on every object
Authentication is not authorization. A logged-in user is still not allowed to touch everyone's data. The classic API breach is a request like GET /api/orders/1043 succeeding for a user who only owns order 1041.
- On every request that references a resource, check that the authenticated principal is actually permitted to act on that specific object. Do not trust the ID in the URL.
- Enforce authorization on the server, in a central place. Hiding a button in the UI is not a control.
- Apply least privilege: scope tokens and roles to the minimum needed, and separate read from write permissions.
- Prefer non-sequential identifiers (such as UUIDs) so resources cannot be trivially enumerated, but treat that as defense in depth, never as your authorization check.
Step 4: Validate and constrain all input
Treat every byte from a client as hostile until proven otherwise. Server-side validation is mandatory; client-side checks are a convenience, not a security boundary.
- Validate against an explicit schema (such as JSON Schema or OpenAPI) and reject anything that does not match, rather than trying to sanitize bad input.
- Use an allow-list of expected fields and types. Reject unknown properties to prevent mass assignment, where a client sets fields like
roleorisAdminthat you never intended to expose. - Always use parameterized queries or an ORM to stop SQL injection; never concatenate input into a query string.
- Set sane limits on body size, array length, and string length, so a single request cannot exhaust memory.
Step 5: Rate-limit and throttle
Without limits, an API invites credential-stuffing, scraping, and denial-of-service. Rate limiting protects both availability and the authentication layer.
- Apply tighter limits to expensive or sensitive endpoints (login, password reset, search) than to cheap reads.
- Key limits by client identity or API key where possible, falling back to IP, and remember a shared proxy can put many users behind one address.
- Return
429 Too Many Requestswith aRetry-Afterheader so well-behaved clients can back off.
For the trade-offs between fixed-window, sliding-window, and token-bucket approaches, see our guide to rate limit strategies for APIs.
Step 6: Lock down headers, CORS, and responses
Several smaller controls close common gaps once the big ones are in place.
- CORS: configure
Access-Control-Allow-Originas a specific allow-list, not*, when requests carry credentials. A wildcard combined with credentials is both insecure and disallowed by browsers. Build correct headers with the CORS Headers Builder. - Security headers: send
Content-Type: application/json,X-Content-Type-Options: nosniff, and a restrictive Content-Security-Policy for any HTML your API returns. - Error hygiene: return generic messages. Stack traces, SQL errors, and internal hostnames hand attackers a map. Log the detail server-side instead.
- Webhook integrity: if you send or receive webhooks, sign payloads with an HMAC and verify the signature on receipt. The HMAC Generator is useful for testing that flow.
Common mistakes that defeat all of the above
Even teams with good intentions repeatedly trip on the same issues:
- Secrets in source control or URLs. Keys in a Git history or query string leak through logs and repos. Keep secrets in environment variables or a secrets manager, and rotate anything exposed.
- Trusting the client. Authorization done only in the front end, or validation done only in JavaScript, is no protection at all.
- "Internal" endpoints with no auth. Anything reachable will eventually be reached; protect debug, admin, and metrics routes too.
- No versioning or deprecation plan. Old, unpatched API versions left running become the soft target.
- Logging sensitive data. Tokens, passwords, and full request bodies in logs turn a log leak into a credential leak.
Security is layered, not a single switch. Start with HTTPS, authentication, and per-object authorization, because they prevent the most damaging breaches, then add input validation, rate limiting, and header hardening. Revisit the list whenever you add an endpoint, because the weakest route defines how secure your API actually is.
Frequently Asked Questions
There is no single step, but if forced to pick one foundation, it is serving everything over HTTPS so credentials and data are never sent in cleartext. The most damaging breaches, however, come from broken authorization, where a user reads another user's records by changing an ID, so per-object authorization checks are the highest-value control to implement alongside HTTPS.
Use API keys for simple server-to-server access where the caller is a trusted application, treating each key like a password (high entropy, hashed at rest, rotatable). Use OAuth 2.0 with bearer tokens or JWTs for user-facing and third-party APIs, because it supports scopes, short-lived tokens, and a separate authorization server. Many systems use both for different audiences.
No. Client-side validation improves user experience but provides zero security, because an attacker can bypass your front end entirely and send raw requests to the API. All validation and authorization must be enforced on the server. Treat client-side checks purely as a convenience layer.
On every request that references a resource by ID, verify on the server that the authenticated user is actually permitted to act on that specific object, and never trust the ID in the URL. This is called object-level authorization, and skipping it is the most common serious API vulnerability. Using non-sequential IDs like UUIDs helps as defense in depth but is not a substitute for the check.
Authentication identifies callers but does not stop them from making too many requests. Rate limiting protects availability against denial-of-service and scraping, and it specifically defends the login endpoint against brute-force and credential-stuffing attacks, which happen before authentication succeeds. Apply stricter limits to sensitive routes and return 429 with a Retry-After header.