Bad APIs create support tickets. Good APIs create fans. Here’s how to design APIs that developers will actually enjoy using.
The Fundamentals# Use Nouns, Not Verbs# # P G P # P G D O E O O E E B S T S G S T L a T T o T E d o T g / d u E c e d u s r t e s e / e U l e r u a s e r s s t e t s e e r e r U s U s s s / e e 1 r r 2 / 3 1 2 3 The HTTP method IS the verb. The URL is the noun.
Plural Resource Names# # G G G G # G G E E E E E E C T T T T N T T o o n / / / / t / / s u u u u u u i s s s s t s s s e e e e h e e t r r r r i r r e s s s s s / s n / / / 1 / t 1 1 1 2 1 2 2 2 3 2 3 3 3 3 / / o o r r r d d d e e e r r r s s / / 4 4 5 5 6 6 Always plural. No exceptions. Consistency beats “correctness.”
Nest Resources Logically# # G # G # G G E E E E U T O T B T T s r u e / d t / r u e o u o ' s r r d s r s e ' d o e d r s e n r e o s r ' s r r / i s t / - d 1 t / 1 i e 2 e 4 g 2 t r 3 m 5 o 3 e s / s 6 / m o / t o s r i o r / d t o d 7 e e e 8 r m d r 9 s s e s e v p 4 a 5 r 6 i / a i n t t e s m s / # 7 8 B 9 e / t v t a e r r i a n t s # T o o m u c h Two levels of nesting is usually the limit. Beyond that, use top-level resources.
HTTP Methods# Use them correctly:
Method Purpose Idempotent Safe GET Read resource Yes Yes POST Create resource No No PUT Replace resource Yes No PATCH Partial update Yes No DELETE Remove resource Yes No
Idempotent: Same request multiple times = same result.
Safe: Doesn’t modify state.
PUT vs PATCH# 1
2
3
4
5
6
7
8
9
10
11
12
13
# PUT: Replace entire resource
PUT /users/123
{
"name" : "Alice" ,
"email" : "alice@example.com" ,
"role" : "admin"
}
# PATCH: Update specific fields
PATCH /users/123
{
"role" : "admin"
}
PUT requires the full resource. PATCH only changes what you send.
Status Codes That Make Sense# Success (2xx)# 2 2 2 0 0 0 0 1 4 O C N K r o e a C t o e n d t e n t - - - G P D E O E T S L , T E T P c E U r T e s , a u t c P e c A d e T e C a d H e r d s e , u s c o n c u o e r t e c h d e i e n d g t o r e t u r n
Client Errors (4xx)# 4 4 4 4 4 4 4 0 0 0 0 0 2 2 0 1 3 4 9 2 9 B U F N C U T a n o o o n o d a r t n p o u b f r R t i F l o M e h d o i c a q o d u c e n u r e n t s y e i n d s s z a R t e b e d l q e u - - - - - e - s I N V R S t n o a e t V s v l s a a a v i o t l - l a d u e i i l r d R d i c c c a d r e o s t s e n y e y c d d f n n r e o l t l t e n e i a i a d t s c x m x e i n t , i , n a ' t t l t ( b e v i s d u d a a , e u t l l x p i s b i l s d u s i e a ( t t c m t s a a i h n t n o o o e t n u t i l e c f d a m a a l a l i b l i l l e o l y e w , d " e w U d e r n t o a c n u . g t ) h e n t i c a t e d " )
Server Errors (5xx)# 5 5 5 5 0 0 0 0 0 2 3 4 I B S G n a e a t d r t e v e r G i w n a c a a t e y l e w U T S a n i e y a m r v e v a o e i u r l t - a E b - r U l r p e U o s p r t - s r t - e T r a e e S m m a o p m m s o e e r t t r a i h v r m i i i e n c l d g e y o b f d u r a o t o i w k l n e e d
Don’t return 200 with {"error": "something broke"}. Use proper status codes.
Request & Response Design# Consistent Response Structure# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Success
{
"data" : {
"id" : "123" ,
"name" : "Alice" ,
"email" : "alice@example.com"
}
}
// Error
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Email is required" ,
"details" : [
{
"field" : "email" ,
"message" : "This field is required"
}
]
}
}
// Collection
{
"data" : [ ... ],
"pagination" : {
"total" : 100 ,
"page" : 1 ,
"per_page" : 20 ,
"next_cursor" : "abc123"
}
}
Same structure every time. Clients shouldn’t have to guess.
Use camelCase (Usually)# 1
2
3
4
5
6
// JavaScript-friendly
{
"userId" : "123" ,
"createdAt" : "2024-03-12T10:00:00Z" ,
"isActive" : true
}
Match the convention of your primary consumers. JavaScript APIs use camelCase. Python APIs might use snake_case. Pick one and stick to it.
ISO 8601 for Dates# 1
2
3
4
{
"createdAt" : "2024-03-12T10:30:00Z" ,
"expiresAt" : "2024-03-19T10:30:00Z"
}
Always UTC. Always ISO 8601. No ambiguity.
Cursor-Based (Preferred)# 1
GET /users?limit= 20& cursor = eyJpZCI6MTIzfQ
Response:
1
2
3
4
5
6
7
{
"data" : [ ... ],
"pagination" : {
"next_cursor" : "eyJpZCI6MTQzfQ" ,
"has_more" : true
}
}
Cursors are:
Stable with real-time data Efficient for large datasets Opaque (implementation can change) Offset-Based (Simpler)# 1
GET /users?page= 3& per_page = 20
Response:
1
2
3
4
5
6
7
8
9
{
"data" : [ ... ],
"pagination" : {
"total" : 156 ,
"page" : 3 ,
"per_page" : 20 ,
"total_pages" : 8
}
}
Simpler for clients, but unstable if data changes between requests.
Filtering and Sorting# Filtering# 1
2
3
4
5
6
7
8
9
10
11
12
# Simple equality
GET /users?role= admin
# Multiple values
GET /users?role= admin,moderator
# Operators
GET /orders?total_gte= 100& total_lte = 500
GET /users?created_after= 2024-01-01
# Search
GET /users?q= alice
Sorting# 1
2
3
4
5
6
7
8
# Single field
GET /users?sort= created_at
# Descending
GET /users?sort= -created_at
# Multiple fields
GET /users?sort= -created_at,name
The - prefix for descending is a common convention.
Field Selection# 1
2
3
4
5
# Only return specific fields
GET /users?fields= id,name,email
# Nested
GET /users?fields= id,name,orders.total
Reduces payload size. Especially useful for mobile clients.
Versioning# URL Path (Recommended)# Simple, explicit, easy to route.
G A E c T c e / p u t s : e r a s p p l i c a t i o n / v n d . m y a p i . v 2 + j s o n
Cleaner URLs, but harder to test and debug.
Query Parameter# G E T / u s e r s ? v e r s i o n = 2
Works, but feels wrong.
My recommendation: URL path versioning. It’s the most pragmatic.
Authentication# Bearer Tokens# 1
2
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
https://api.example.com/users
Standard, works everywhere, easy to implement.
API Keys# 1
2
3
4
5
6
# Header (preferred)
curl -H "X-API-Key: sk_live_abc123" \
https://api.example.com/users
# Query param (avoid - appears in logs)
curl https://api.example.com/users?api_key= sk_live_abc123
Use headers for API keys. Query params leak into access logs.
Error Handling# Be Specific# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Bad
{
"error" : "Invalid request"
}
// Good
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Validation failed" ,
"details" : [
{
"field" : "email" ,
"code" : "INVALID_FORMAT" ,
"message" : "Email must be a valid email address"
},
{
"field" : "age" ,
"code" : "OUT_OF_RANGE" ,
"message" : "Age must be between 18 and 120"
}
]
}
}
Machine-readable codes for programmatic handling. Human-readable messages for debugging.
Document Error Codes# 1
2
3
4
5
6
7
8
9
## Error Codes
| Code | HTTP Status | Description |
|------|-------------|-------------|
| VALIDATION_ERROR | 400 | Request body failed validation |
| AUTHENTICATION_REQUIRED | 401 | No valid credentials provided |
| PERMISSION_DENIED | 403 | Insufficient permissions |
| RESOURCE_NOT_FOUND | 404 | Requested resource doesn't exist |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests |
Rate Limiting# H X X X T - - - T R R R P a a a / t t t 1 e e e . L L L 1 i i i m m m 2 i i i 0 t t t 0 - - - L R R O i e e K m m s i a e t i t : n : i 1 n 1 0 g 7 0 : 1 0 0 9 2 9 4 9 4 8 0 0
Return Retry-After on 429# H R { } T e T t " } P r e / y r " " 1 - r c m . A o o e 1 f r d s t " e s 4 e : " a 2 r : g 9 : { e " " T 6 R : o 0 A o T " E T M _ o a L o n I y M m I a R T n e _ y q E u X r e C e s E q t E u s D e E s D t " s , . R e t r y a f t e r 6 0 s e c o n d s . "
Documentation# OpenAPI/Swagger# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
openapi : 3.0.0
info :
title : My API
version : 1.0.0
paths :
/users :
get :
summary : List users
parameters :
- name : role
in : query
schema :
type : string
enum : [ admin, user, moderator]
responses :
'200' :
description : List of users
content :
application/json :
schema :
$ref : '#/components/schemas/UserList'
Generate docs, SDKs, and mock servers from the same spec.
Examples Everywhere# 1
2
3
4
5
6
7
8
# Show real examples, not just types
examples :
createUser :
summary : Create a new user
value :
name : "Alice Smith"
email : "alice@example.com"
role : "admin"
Developers copy-paste examples. Make them work.
The Checklist# Before shipping your API:
Start Here# Today: Audit one endpoint for consistencyThis week: Add proper error responsesThis month: Generate OpenAPI specThis quarter: Build a developer portalThe best APIs are the ones developers figure out without reading the docs. Design for intuition.
A great API is invisible. Developers just use it and it works exactly how they expected.