Disclosure: This article was created or edited with AI assistance. Revised on 13 October 2026 to remove promotional links and unsupported outcome claims. It is educational guidance, not a verified personal case study or a measured result. Examples are illustrative; use only your own documented experience and measurements.
The sections below present options and illustrative examples. Adapt them to the requirements of your project or role; they do not establish a universal rule or promise a result.
Rule 1: Use Nouns for Endpoints, Not Verbs
The HTTP method already tells you the action. The URL should describe the resource.
Bad:
GET /getUsers
POST /createUser
PUT /updateUser/123
DELETE /deleteUser/123
GET /getAllOrdersForUser/123
Good:
GET /users
POST /users
PUT /users/123
DELETE /users/123
GET /users/123/orders
The verb is already in the HTTP method (GET, POST, PUT, DELETE). Putting it in the URL too is redundant. Think of URLs as addresses to resources, not function names.
Rule 2: Use Plural Nouns
This one seems small but it eliminates a whole category of confusion.
Bad (inconsistent):
GET /user -- all users? one user? who knows
GET /user/123 -- ok this is one user
GET /users -- wait, different endpoint for the list?
Good (consistent):
GET /users -- list of users
GET /users/123 -- single user
POST /users -- create a user
Plural resource names are a common convention, not a protocol requirement. /users returns a list. /users/123 returns one from that list. Simple and predictable.
Rule 3: Use Proper HTTP Status Codes
Do not just return 200 for everything. Status codes exist for a reason and every HTTP client knows how to handle them.
200 OK -- Successful GET, PUT, PATCH
201 Created -- Successful POST that creates a resource
204 No Content -- Successful DELETE
400 Bad Request -- Client sent invalid data
401 Unauthorized -- No valid authentication
403 Forbidden -- Authenticated but not allowed
404 Not Found -- Resource doesn't exist
409 Conflict -- Resource state conflict (duplicate email, etc.)
422 Unprocessable -- Validation failed
429 Too Many Req -- Rate limit exceeded
500 Internal Error -- Something broke on the server
Bad:
HTTP 200 OK
{
"success": false,
"error": "User not found"
}
Good:
HTTP 404 Not Found
{
"error": {
"code": "USER_NOT_FOUND",
"message": "No user found with id 123"
}
}
Your API consumers will write cleaner code because they can rely on status codes instead of parsing your response body for hidden error messages.
Rule 4: Version Your API From Day One
Plan how the API will evolve. Additive compatible changes and breaking changes need different handling; URL versioning is one option.
Common approaches:
# URL versioning (most common, my preference)
GET /v1/users
GET /v2/users
# Header versioning
GET /users
Accept: application/vnd.myapi.v1+json
# Query parameter
GET /users?version=1
I go with URL versioning because it's the most obvious and easiest to test. You can literally see the version in the browser URL bar.
Start with /v1/ and when you need breaking changes, create /v2/. Keep /v1/ running until clients migrate.
Rule 5: Use Pagination, Filtering, and Sorting Via Query Parameters
Pagination:
GET /users?page=2&per_page=20
Response:
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 1234,
"total_pages": 62
}
}
Filtering:
GET /users?status=active&role=admin
GET /orders?created_after=2026-01-01&status=shipped
Sorting:
GET /users?sort=created_at&order=desc
GET /products?sort=price&order=asc
Always set a default page size and a maximum page size. Don't let someone request per_page=1000000.
# Framework-neutral validation result. The route must honor the returned status.
def pagination_response(args):
try:
page = int(args.get("page", 1))
per_page = int(args.get("per_page", 20))
except (TypeError, ValueError):
return {"error": "page and per_page must be positive integers"}, 400
if page < 1 or per_page < 1:
return {"error": "page and per_page must be positive integers"}, 400
return {"page": page, "per_page": min(100, per_page)}, 200
Rule 6: Return Consistent Error Responses
Every error from your API should follow the same format. Don't make clients guess what shape the error will be.
Bad (inconsistent):
// Sometimes this
{"error": "Not found"}
// Sometimes this
{"message": "Validation failed", "details": ["Email required"]}
// Sometimes this
{"success": false, "reason": "Server error"}
Good (always the same shape):
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Email is required"
},
{
"field": "age",
"message": "Age must be a positive number"
}
]
}
}
Pick a format and stick to it. Include:
- A machine-readable error code
- A human-readable message
- Details for validation errors (which field, what's wrong)
Rule 7: Use Proper Nesting for Related Resources
Resources that belong to other resources should be nested in the URL. But don't go more than 2 levels deep.
Good:
GET /users/123/orders -- orders for user 123
GET /users/123/orders/456 -- specific order for user 123
POST /users/123/orders -- create order for user 123
Too deep (bad):
GET /users/123/orders/456/items/789/reviews/012
If you're going more than 2 levels deep, flatten it:
GET /order-items/789/reviews
Or use query parameters:
GET /reviews?order_item_id=789
Deep nesting makes URLs long, hard to read, and annoying to work with in code.
Rule 8: Accept and Return JSON
In 2026, JSON is the standard for REST APIs. Unless you have a very specific reason, use JSON for both request and response bodies.
Example headers for a JSON response:
Content-Type: application/json
Accept: application/json
Use consistent key naming. Pick one and stick with it:
// snake_case (Python/Ruby convention)
{
"first_name": "Daniil",
"created_at": "2026-03-01T12:00:00Z"
}
// camelCase (JavaScript convention)
{
"firstName": "Daniil",
"createdAt": "2026-03-01T12:00:00Z"
}
I use snake_case because most of my backends are Python, but the convention depends on your ecosystem. Just be consistent.
Document the date representation and timezone. An ISO 8601 timestamp is a common choice; an epoch timestamp can also be valid when its unit and meaning are explicit.
Rule 9: Implement Rate Limiting
If your API is public (or even internal), you need rate limiting. Without it, one misbehaving client can take down your entire service.
Return rate limit info in headers:
HTTP 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1709312400
When the limit is hit:
HTTP 429 Too Many Requests
Retry-After: 30
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Try again in 30 seconds."
}
}
Simple implementation with a middleware:
from functools import wraps
from time import time
rate_limits = {}
def rate_limit(max_requests=100, window=3600):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
client_ip = request.remote_addr
now = time()
if client_ip not in rate_limits:
rate_limits[client_ip] = []
# Remove old requests outside the window
rate_limits[client_ip] = [
t for t in rate_limits[client_ip]
if now - t < window
]
if len(rate_limits[client_ip]) >= max_requests:
return jsonify({
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": f"Max {max_requests} requests per {window}s"
}
}), 429
rate_limits[client_ip].append(now)
return f(*args, **kwargs)
return wrapper
return decorator
For production, use Redis or a proper rate limiting service instead of an in-memory dict. But you get the idea.
Rule 10: Document Your API Like Someone's Life Depends On It
Because someone's evening definitely depends on it. Bad API docs mean frustrated developers, more support tickets, and fewer people using your API.
At minimum, document:
- Every endpoint with its URL, method, and description
- Request parameters (path, query, body) with types and requirements
- Response format with example responses
- Error codes and what they mean
- Authentication method
Use OpenAPI/Swagger:
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/users:
get:
summary: List all users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: List of users
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
Tools like Swagger UI can generate interactive documentation from your OpenAPI spec. Clients can test endpoints directly in the browser.
Bonus: The Golden Rule
Design your API as if you're the one who has to consume it.
Before finalizing any endpoint, try writing the client code. If it feels awkward, the API design is probably wrong.
// Does this feel natural?
const user = await api.get('/users/123');
const orders = await api.get('/users/123/orders?status=active&sort=date&order=desc');
await api.post('/users', { name: 'Daniil', email: 'dan@example.com' });
await api.delete('/users/123');
If you can read that code and immediately understand what it does without looking at docs, you've designed a good API.
TL;DR
- Nouns for URLs, verbs come from HTTP methods
-
Consistent resource naming, such as
/users - Proper status codes (stop returning 200 for errors)
-
Plan compatibility and breaking changes;
/v1/usersis one option - Bound collection responses and document pagination
- Consistent error format across all endpoints
- Keep resource relationships and routes understandable
- Choose and document representations with consistent key naming
- Rate limit and tell clients about it in headers
- Document like your users' sanity matters
Top comments (0)