API endpoint security: a practical checklist
Most API breaches come from a short list of mistakes: missing authorization checks, trusted input and no rate limits. A checklist to review every endpoint against.
APIs carry the data that matters: orders, payments, personal details. They are also easy to probe, because an attacker needs no browser, only a script. Most incidents trace back to a small set of mistakes that repeat across companies. Review each endpoint against the eight points below.
1. Authenticate every request
Every endpoint that is not deliberately public must know who is calling. Use OAuth 2.0 or OpenID Connect tokens for users and scoped keys for server to server calls. There is no such thing as an internal endpoint that needs no check. Networks get misconfigured.
2. Authorize the object, not only the route
This is the most common serious API flaw. The user is logged in, the route is allowed, and nobody checks whether this order belongs to this user. Changing an ID in the URL then returns someone else's data.
app.get("/orders/:id", requireUser, async (req, res) => {
// The owner is part of the query, so another user's order is never found
const order = await db.orders.findOne({ id: req.params.id, userId: req.user.id });
if (!order) return res.status(404).end();
res.json(order);
});
Apply the same check to updates and deletes.
3. Validate all input
Treat everything from the client as untrusted: body, query string, headers and path parameters.
import { z } from "zod";
const OrderInput = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100),
});
const input = OrderInput.parse(req.body); // throws on anything unexpected
Check types, lengths and ranges, reject fields you do not expect, and always use parameterized queries for the database.
4. Limit the rate
Without limits, an attacker can try a million passwords or simply exhaust your servers.
- Limit per user, per API key and per IP address.
- Use stricter limits on login, sign up and password reset.
- Answer with status 429 and a
Retry-Afterheader.
5. Encrypt everything in transit
Serve the API over HTTPS only, redirect or refuse plain HTTP, and send the HSTS header. This applies to calls between your own services as well.
6. Return only what the client needs
- Build responses from an explicit list of fields. Do not serialize the database record.
- Never return password hashes, internal IDs or other users' data by accident.
- Keep error messages generic. A stack trace is a map for an attacker.
7. Log and watch
Record authentication failures, permission denials and unusual volumes, with a request ID and without secrets. A sudden rise in 401 or 403 responses is often the first sign of an attack.
8. Know what you expose
Old versions and forgotten test endpoints are a favourite way in. Keep an inventory of your endpoints, remove the ones you no longer need, and scan regularly with a tool such as OWASP ZAP.
The checklist
| Check | Question to ask |
|---|---|
| Authentication | Does the endpoint reject a request with no token? |
| Authorization | Can user A read or change user B's record? |
| Validation | What happens with a missing, huge or malformed value? |
| Rate limit | How many requests per minute are allowed? |
| Transport | Is plain HTTP refused? |
| Response | Does it return more fields than the screen shows? |
| Logging | Would we notice a hundred failed logins? |
| Inventory | Is this endpoint documented and still needed? |
Related reading: JWT best practices and keeping API keys out of your frontend.