A badly designed API is a source of endless frustration — for the developers who consume it and eventually for the team that maintains it. Inconsistent naming, wrong status codes, no versioning strategy, and cryptic error messages are the four horsemen of API technical debt.
None of this is hard to get right if you apply a few principles consistently from the start.
Resource Naming: Nouns, Not Verbs
The single most important REST convention is that URLs represent resources, not actions. The HTTP method conveys the action.
# Bad — verb in the URL
GET /getUser/123
POST /createOrder
DELETE /deleteProduct/456
# Good — noun-based resources
GET /users/123
POST /orders
DELETE /products/456
Use plural nouns for collections. /users not /user, /orders not /order. This is the near-universal convention, and consistency matters more than which convention you pick.
Nested Resources
When one resource belongs to another, nest the URL to express that relationship:
GET /users/123/orders # all orders for user 123
GET /users/123/orders/456 # specific order 456 for user 123
POST /users/123/orders # create an order for user 123
Keep nesting shallow — two levels is usually the limit. If you need /users/123/orders/456/items/789/reviews, that's a sign to flatten. /reviews/789 with query parameters for filtering is probably cleaner.
HTTP Methods and What They Mean
HTTP methods aren't just suggestions — use them semantically.
GET — Retrieve a resource or collection. Must be safe (no side effects) and idempotent (same result each time).
POST — Create a new resource. The response should return the created resource and a 201 Created status with a Location header pointing to the new URL.
PUT — Replace a resource entirely. Idempotent — sending the same PUT twice should produce the same result.
PATCH — Partially update a resource. Send only the fields you want to change.
DELETE — Remove a resource. Idempotent — the effect on server state is the same regardless of how many times you call it (the resource remains absent). A second DELETE on an already-deleted resource may return 404 or 204 depending on your implementation; both are acceptable. What matters is that the server never returns 500 for a repeat delete.
GET /products → list all products
POST /products → create a product
GET /products/42 → get product 42
PUT /products/42 → replace product 42 entirely
PATCH /products/42 → update specific fields of product 42
DELETE /products/42 → delete product 42
Status Codes: Use Them Precisely
Status codes carry semantic weight. Returning 200 OK for an error with an error body forces clients to parse every response body before they know whether the request succeeded — don't do this.
The most important ones to get right:
200 OK— request succeeded, body contains the result201 Created— resource was created (POST); includeLocationheader204 No Content— success with no body (DELETE, some PATCHes)400 Bad Request— client sent invalid input401 Unauthorized— not authenticated (confusingly named)403 Forbidden— authenticated but not allowed404 Not Found— resource doesn't exist409 Conflict— state conflict (e.g., duplicate unique field)422 Unprocessable Entity— input is syntactically valid but semantically wrong429 Too Many Requests— rate limit hit; includeRetry-Afterheader500 Internal Server Error— something broke server-side
Read HTTP Status Codes: The Complete Developer Reference for a deeper breakdown of the ones developers most often confuse.
Consistent Error Response Format
Define an error shape and use it everywhere. Clients should be able to write one error-handling function that works across your entire API.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "age",
"message": "Must be a positive integer"
}
]
}
}
code is a stable machine-readable string. message is human-readable. details gives field-level context for validation failures. Keep code values in a documented enum so clients can handle them programmatically.
You can validate your error payloads against your expected shape easily using the JSON Formatter — paste in a response and inspect the structure.
Versioning Strategies
You'll need to make breaking changes eventually. Having a versioning strategy from day one is how you avoid the painful "v0 is still running in production" situation.
Path versioning (/v1/users) is the most common approach. It's explicit, easy to route, and visible in logs. URLs change when you bump versions, but for most APIs that's a worthwhile tradeoff for the simplicity.
Header versioning (Accept: application/vnd.api+json;version=2) keeps URLs clean but is harder to test in a browser and less discoverable.
Query parameter versioning (/users?version=2) is occasionally used but mixes resource addressing with protocol negotiation — generally not worth it.
Path versioning wins on practicality. Increment the version for breaking changes — removed fields, changed field types, altered behavior. New optional fields or new endpoints don't need a bump.
Pagination
Never return unbounded collections. A GET /orders that returns 50,000 records will eventually cause problems.
Offset pagination is the simplest approach:
GET /orders?limit=20&offset=40
Response should include metadata:
{
"data": [...],
"pagination": {
"total": 342,
"limit": 20,
"offset": 40,
"nextOffset": 60
}
}
Cursor pagination is better for real-time data where rows might be inserted between pages:
GET /orders?limit=20&after=cursor_abc123
The cursor is an opaque string (often a base64-encoded timestamp+ID) returned in the previous response. Clients don't construct it — they just pass back what they received.
Use offset for admin interfaces and reports where users jump to specific pages. Use cursor for feeds, timelines, and any data with frequent inserts.
Request and Response Bodies
Keep request bodies clean. For a URL-encoded resource path, the ID is in the URL — don't repeat it in the body.
Use consistent field naming. camelCase (firstName) is standard in JSON APIs. snake_case (first_name) is common in Python-backed APIs. Pick one and apply it everywhere.
Dates should be ISO 8601 strings: "2024-01-15T10:30:00Z". Unix timestamps are fine for internal systems but ISO strings are more readable and universally parseable.
When sending data to your API, use the URL Encoder tool to properly encode query parameter values — special characters in filter values are a common source of bugs.
HATEOAS
HATEOAS (Hypermedia as the Engine of Application State) is a REST constraint where responses include links to related actions:
{
"id": 123,
"status": "pending",
"_links": {
"self": "/orders/123",
"cancel": "/orders/123/cancel",
"items": "/orders/123/items"
}
}
In theory, a fully HATEOAS API is self-discoverable. In practice, almost no production REST APIs implement the full thing — clients get built against documented contracts, not discovered dynamically. Adding _links for the most common related actions is a pragmatic middle ground that's worth adopting.
Wrapping Up
Good REST API design comes down to consistency. Resources are nouns, methods convey action, status codes carry semantics, errors have a predictable shape, and versioning is planned ahead. When you're building or debugging API payloads, the JSON Formatter makes it easy to inspect response bodies and catch structural issues before they become integration bugs.