An API is a user interface for developers. Like any interface, small inconsistencies add up to confusion, bugs and support requests. The good news is that most REST API problems come from a short list of mistakes. Here they are, each with a better practice you can apply today.
Mistake 1: Verbs in URLs
POST /createArticle
GET /getArticleById?id=42
POST /deleteArticle/42
HTTP already has verbs. URLs should name resources (nouns), and the method says what to do with them.
POST /articles create
GET /articles/42 read one
GET /articles list
PATCH /articles/42 partial update
DELETE /articles/42 delete
Use plural nouns consistently and nest only when the relationship is real and shallow, such as /articles/42/comments. Avoid deep chains like /users/7/articles/42/comments/3/likes.
Mistake 2: Everything returns 200
Returning 200 OK with {"success": false} forces every client to parse the body to discover an error, and it hides problems from monitoring tools. Status codes are part of your contract.
| Situation | Status |
|---|---|
| Read succeeded | 200 OK |
| Resource created | 201 Created (+ Location header) |
| Accepted for async processing | 202 Accepted |
| Success, nothing to return | 204 No Content |
| Invalid input | 400 Bad Request or 422 Unprocessable Content |
| Not authenticated | 401 Unauthorized |
| Authenticated but not allowed | 403 Forbidden |
| Does not exist | 404 Not Found |
| Conflicts with current state | 409 Conflict |
| Rate limit reached | 429 Too Many Requests |
| Server bug | 500 Internal Server Error |
Mistake 3: Inconsistent error bodies
If one endpoint returns {"error": "bad"} and another returns {"message": "...", "code": 17}, clients need special cases everywhere. Pick one shape. The Problem Details format (RFC 9457) is a well-supported standard:
{
"type": "https://api.example.com/problems/validation",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields are invalid.",
"errors": [
{ "field": "slug", "message": "must be unique" },
{ "field": "title", "message": "must not be blank" }
]
}
Make errors actionable: say which field failed and why. Never leak stack traces or SQL to clients.
Mistake 4: Unbounded lists
GET /articles that returns every row works in development and falls over in production. Paginate every collection from day one.
- Offset pagination (
?page=3&per_page=20) is simple and fine for small, stable datasets. - Cursor pagination (
?after=eyJpZCI6MTIzfQ&limit=20) stays fast on large tables and does not skip or duplicate items when data changes between requests.
{
"data": [ { "id": 124, "title": "..." } ],
"page": { "next_cursor": "eyJpZCI6MTQ0fQ", "has_more": true }
}
Set a sensible default page size and a maximum, and document both.
Mistake 5: Ignoring idempotency
Networks fail. Clients retry. If a retry of POST /payments creates a second payment, you have a serious bug. GET, PUT and DELETE are idempotent by definition; POST is not.
For operations that must not happen twice, accept an idempotency key header. The server stores the key with the result and returns the same response for repeated requests with the same key.
POST /payments
Idempotency-Key: 5f1c2d7e-9b1a-4c0e-8d53-2a1b9e6f7c10
Content-Type: application/json
Mistake 6: Breaking clients without warning
Renaming a field or changing its type breaks every client that depends on it. Strategies that help:
- Additive changes are safe: adding optional fields or new endpoints rarely breaks anyone.
- Version when you must break: a URL prefix (
/v2/articles) is the most visible and easiest to route; a header-based version is cleaner but harder to test in a browser. - Deprecate before removing: announce, add a
Deprecationheader or documentation note, monitor usage, then remove.
Mistake 7: Over- and under-fetching
Returning huge nested objects wastes bandwidth; returning too little forces clients into many round trips. Useful middle grounds:
- Sparse fieldsets:
GET /articles?fields=id,title,slug - Explicit expansion:
GET /articles/42?include=author - Purpose-built endpoints for heavy screens when needed.
Mistake 8: Weak security defaults
- Serve everything over HTTPS only.
- Authenticate with standard mechanisms (OAuth 2.0 / OpenID Connect, or signed tokens) — do not invent your own.
- Authorise every request at the resource level: can this user access this article?
- Validate and limit input sizes; reject unknown fields where it makes sense.
- Rate-limit and return
429with aRetry-Afterheader.
Document it like a product
Describe the API with OpenAPI. A spec file lets you generate reference documentation, client SDKs and contract tests, and it forces the team to agree on names and shapes before code is written.
paths:
/articles/{id}:
get:
summary: Get an article
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
"200": { description: The article }
"404": { description: Not found }
A good API is boring in the best way: predictable URLs, honest status codes, consistent errors and changes that never surprise the people who depend on it.
Comments