I'm always excited to take on new projects and collaborate with innovative minds.
A practical guide to the most common REST API design mistakes developers make — from inconsistent naming and poor versioning to weak security and unclear error handling. Learn how to build cleaner, more reliable, and scalable APIs with intentional design choices.
Building a REST-style API seems straightforward: set up endpoints, expose resources, and off you go. But in practice, the devil is in the details. Tiny mis-steps add friction, frustrate developers, and make future changes painful. Here are five of the most frequent mistakes I’ve seen — and how you can dodge them.
One of the first things API consumers see is your URL structure and naming conventions. If you’re inconsistent, you force everybody to check documentation every time. One day the end-point is /users, next day it’s /userList, or perhaps /order-list. This inconsistency kills the developer experience.
What works better:
/users, /products./users/{id}/settings).GET /entries?userId={userId}&habitId={habitId} rather than GET /users/{userId}/habits/{habitId}/entries. Wrap arrays inside a metadata object so you can add pagination, cursors, or extra info later without breaking clients. For example:
{
"data": [
{ ... },
{ ... }
],
"total": 42,
"hasMore": true,
"nextCursor": "cursor_01J9KaBcd"
}
``` :contentReference[oaicite:3]{index=3}
Versioning APIs is tricky. Many teams default to something like /v1/users, /v2/users, etc. The intention is good — you want to evolve without breaking clients — but in practice it becomes a maintenance nightmare. You end up supporting multiple versions, patches in three places, diverging docs, confused support tickets… and worse, you often use versioning as an excuse for laziness: rather than thinking how to evolve the API without breaking, you just bump the version and force clients to rewrite.
Better approach:
Instead of breaking existing endpoints, extend them: keep old fields, mark them deprecated in documentation, and add new fields. Example:
// original
{ "id": 1, "name": "John Doe" }
// later version
{
"id": 1,
"name": "John Doe", // still there
"firstName": "John",
"lastName": "Doe"
}
GET /users/{id}?include=habits,entries or ?format=detailed rather than creating a full v2./users/{id} stays the same; you introduce /userProfiles/{id} for the new format. In any case, when you deprecate something you owe your consumers good notice (6-12 months) and a migration guide. An endpoint might work fine when your dataset is small (say 10 entries). But soon enough you have thousands or tens of thousands. If a client calls GET /entries and you respond with the full list, you’ll kill performance, exhaust mobile users’ data plans, and frustrate everyone.
What to design from the start:
GET /entries?habitId=123&date=2025-08-01&status=completed. That way they don’t download the whole dataset and sift through it client-side.GET /entries/search?q=morning+run+park. Use separate endpoints or logic so your engine optimises for full-text search rather than mixing with heavy filtering. GET /entries?offset=100&limit=50 (simple, widely understood—but has problems: if items are added/deleted mid-pagination you get duplicates or misses; also offsetting high values is expensive) GET /entries?limit=50&cursor=eyJpZCI6MTIzfQ== — more complex to implement but more reliable for large dynamic datasets.When things go wrong, how your API responds matters a lot to the clients consuming it. A vague error message like {"error":"An error occurred"} is useless — it leaves client developers guessing.
Best practices to adopt:
Use a structured error format that clearly indicates what happened, why it happened, and how to fix it. For example:
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation Failed",
"status": 400,
"detail": "The request body contains invalid fields",
"instance": "/habits/123",
"errors": [
{
"field": "name",
"reason": "Must be between 1 and 100 characters",
"value": ""
},
{
"field": "frequency",
"reason": "Must be one of: daily, weekly, monthly",
"value": "sometimes"
}
]
}
400 Bad Request – your input is garbage401 Unauthorized – you’re not logged in / unauthenticated403 Forbidden – you’re authenticated but not allowed404 Not Found – resource doesn’t exist409 Conflict – resource state conflict429 Too Many Requests – you hit rate limit500 Internal Server Error – your fault (server)503 Service Unavailable – come back laterSecurity is not an “extra feature” you add in phase two. If you wait, you risk data leaks, compliance failures, poor user trust — and retrofitting security is harder than building it in from the start.
Security fundamentals to bake in early:
429 Too Many Requests when thresholds are exceeded. You can later fine-tune limits per endpoint or user tier. Good API design isn’t about perfection or blindly following all “best practices”. It’s about making intentional, thoughtful decisions about your trade-offs. You’ll always face constraints: consistency might reduce flexibility; security might slow things; stability might hamper innovation. What matters is that you know why you made each decision, document it, stay consistent, and design for evolution rather than trying to predict some perfect future.
Your API is a promise to the developers who use it. Every time you break that promise — whether by changing without warning, returning inconsistent patterns, or giving useless errors — you weaken trust. Build the kind of API you yourself would want to use: your fellow developers will thank you, your support team will breathe easier — and future you will be grateful.
Your email address will not be published. Required fields are marked *