I've built probably 20+ REST APIs at this point. The first few were terrible. Like "return everything as a 200 with an error message in the body" terrible.
Over time, through a lot of trial and error (mostly error), I've collected a set of rules that make APIs actually pleasant to work with. I'm Daniil, 19, and these are the 10 REST API design rules I wish someone had just told me from the start.
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
Always plural. /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.
Here are the ones you'll use 90% of the time:
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
You will change your API. It's not a question of if, it's when. If you don't version it, you'll break every client that uses it.
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
Never return an unbounded list. I learned this the hard way when an endpoint that "only had a few hundred records" suddenly had 50,000 and the response was 12MB.
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.
# In your controller
page = max(1, int(request.args.get('page', 1)))
per_page = min(100, int(request.args.get('per_page', 20)))
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.
Always set these headers:
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.
Also: always use ISO 8601 for dates (2026-03-01T12:00:00Z). Not unix timestamps, not 03/01/2026, not some custom format. ISO 8601.
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
-
Plural nouns always (
/usersnot/user) - Proper status codes (stop returning 200 for errors)
-
Version from day one (
/v1/users) - Paginate everything with query params
- Consistent error format across all endpoints
- Nest resources but max 2 levels
- JSON everything with consistent key naming
- Rate limit and tell clients about it in headers
- Document like your users' sanity matters
Follow these 10 rules and your API will be better than 90% of what's out there. Seriously.
If you found this useful, I share more stuff like this on Telegram and sell developer toolkits on Boosty.
Top comments (0)