A well-designed API feels obvious. Endpoints are predictable, responses are consistent, errors are helpful. A poorly designed API creates confusion, support tickets, and workarounds.
These conventions create APIs that are intuitive to integrate.
Resource Naming# Use nouns, not verbs. Plural for collections:
β
G G P P D β G P G P E E O U E E O E O G T T S T L B T S T S o T E a T T o T d d E : : / / / / / / / / / u u u u u g c u u s s s s s e r s s e e e e e t e e e r r r r r U a r r s s s s s s t / s / / / e e 1 / 1 1 1 r U 2 1 2 2 2 s s 3 2 3 3 3 e 3 r / d e l e t # # # # # e L G C U D i e r p e s t e d l t a a e u t t t u s e e e s e e r u u u r s s s s 1 e e e 2 r r r 3 1 1 2 2 3 3
Nested resources for relationships:
G G P E E O T T S T / / / u p u s o s e s e r t r s s s / / / 1 4 1 2 5 2 3 6 3 / / / p c p o o o s m s t m t s e s n t s # # # P C C o o r s m e t m a s e t n e b t y s p o u o s s n t e r p f o o 1 s r 2 t 3 u 4 s 5 e 6 r 1 2 3
Consistent Response Structure# Every response follows the same pattern:
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
34
35
// Success
{
"data" : {
"id" : "123" ,
"type" : "user" ,
"attributes" : {
"name" : "Alice" ,
"email" : "alice@example.com"
}
}
}
// Collection
{
"data" : [
{ "id" : "123" , "type" : "user" , "attributes" : { ... }},
{ "id" : "124" , "type" : "user" , "attributes" : { ... }}
],
"meta" : {
"total" : 100 ,
"page" : 1 ,
"per_page" : 20
}
}
// Error
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Email is required" ,
"details" : [
{ "field" : "email" , "message" : "cannot be blank" }
]
}
}
Clients should always know where to find data, metadata, and errors.
HTTP Status Codes# Use them correctly:
2 2 2 4 4 4 4 4 4 5 5 5 0 0 0 0 0 0 0 0 2 0 0 0 0 1 4 0 1 3 4 9 2 0 2 3 O C N B U F N C U I B S K r o a n o o o n n a e e d a r t n p t d r a C u b f r e v t o R t i F l o r G i e n e h d o i c n a c d t q o d u c e a t e e u r e n t s l e n e i n d s w U t s z a E a n t e b r y a d l r v e o a - - - - - - - - r i - l S R S I A A R S - - a u e u n u u e t V b c s c v t t s a a S U l c o c a h h o t l e p e e u e l e e u e i r s s r s i n n r d v t - s c s d t t c c a e r e , i i e o t r e T ( i c c n i a e G c n n a a d f o b m m E r o p t t o l n u p T e u i e e i g s o , a b t o d s c e e r t o n n t r r a P e d b ' r v r U d y r u t ( o i i T e t d r c l , ( ( q e u s e y P D u n x p P O E i o i l f d A S L r t s i a o T T E e t c i w C ) T d a a l n H E l t e ) ) l e d o , w e e d t c . )
Don’t return 200 with {"success": false}. Use proper status codes.
Always paginate collections:
G E T / u s e r s ? p a g e = 2 & p e r _ p a g e = 2 0
Response includes pagination metadata:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"data" : [ ... ],
"meta" : {
"pagination" : {
"total" : 253 ,
"page" : 2 ,
"per_page" : 20 ,
"total_pages" : 13
}
},
"links" : {
"self" : "/users?page=2&per_page=20" ,
"first" : "/users?page=1&per_page=20" ,
"prev" : "/users?page=1&per_page=20" ,
"next" : "/users?page=3&per_page=20" ,
"last" : "/users?page=13&per_page=20"
}
}
For large datasets, use cursor-based pagination:
G E T / e v e n t s ? c u r s o r = e y J p Z C I 6 M T I z f Q & l i m i t = 5 0
Filtering and Sorting# Consistent query parameter patterns:
# G G G # G G G # G # G E E E E E E E E F T T T S T T T F T I T i o i n l / / / r / / / e / c / t u u u t u u u l u l u e s s s i s s s d s u s r e e e n e e e e d e i r r r g r r r s r i r n s s s s s s e s n s g ? ? ? ? ? ? l ? g ? s r c s s s e f i t o r o o o c i r n a l e r r r t e e c t e a t t t i l l l u = t = = = o d a u s a e c - n n s t d = d d r c a = e e a m _ e r m i d = c i a a e e d p t n f t a , , r o i & t e t - n e s v s e d e c a s t e t r _ d r m o s a = a _ e e u , t 2 t a a , r p u 0 t t e c r s 2 e m e o = 4 d a s f a - _ i i c 0 a l l t 1 t e i - v 0 # # # e 1 A D M s e u c s l e c t n e i d n p i d l n i e g n g f i e l d s
Error Responses# Errors should be helpful:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Request validation failed" ,
"details" : [
{
"field" : "email" ,
"code" : "INVALID_FORMAT" ,
"message" : "Must be a valid email address"
},
{
"field" : "age" ,
"code" : "OUT_OF_RANGE" ,
"message" : "Must be between 18 and 120"
}
],
"request_id" : "req_abc123"
}
}
Include:
Machine-readable error code Human-readable message Field-level details for validation errors Request ID for debugging Versioning# Version your API from day one:
# G G # G A E E E c U T T H T c R e e L a / p v d u t p 1 2 e s : a / / r e t u u r a h s s v s p e e e p v r r r l e s s s i r i c s o a i n t o i i n n o i g n n / g v n ( d r . e m c y o a m p m i e . n v d 2 e + d j ) s o n Authentication# Use standard mechanisms:
# G X # G A E - E u A T A B T t P P e h I / I a / o u - r u r K s K e s i e e e r e z y r y r a s : T s t ( o i s s k o i k e n m _ n : p l l i ( B e v O e ) e A a _ u r a t e b h r c 2 1 / e 2 J y 3 W J T h ) b G c i O i J I U z I 1 N i I s . . .
Return 401 with clear error:
1
2
3
4
5
6
{
"error" : {
"code" : "INVALID_TOKEN" ,
"message" : "The access token has expired"
}
}
Rate Limiting# Return rate limit info in headers:
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 6 0 : 4 0 0 9 0 9 0 7 0 0 0 0
When exceeded:
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 R M _ a a L t n I e y M I l R T i e _ m q E i u X t e C s E e t E x s D c E e D e " d , e d . R e t r y a f t e r 6 0 s e c o n d s . "
Timestamps# Use ISO 8601 with timezone:
1
2
3
4
{
"created_at" : "2024-01-15T10:30:00Z" ,
"updated_at" : "2024-01-15T14:22:15Z"
}
Always use UTC. Let clients convert to local time.
Null vs Absent# Be consistent about null values:
1
2
3
4
5
6
7
8
9
10
11
// Option 1: Include null fields
{
"name" : "Alice" ,
"nickname" : null ,
"avatar_url" : null
}
// Option 2: Omit null fields
{
"name" : "Alice"
}
Pick one approach and stick to it. Document your choice.
Bulk Operations# For batch operations, use a consistent pattern:
P C { } O o S n " ] T t o e p { { { / n e " " " u t r m m m s - a e e e e T t t t t r y i h h h s p o o o o / e n d d d b : s " " " u " : : : l a : k p " " " p [ c u d l r p e i e d l c a a e a t t t t e e e i " " " o , , , n / " " " j d i i s a d d o t " " n a : : " : " " 1 4 { 2 5 " 3 6 n " " a , } m e " " d : a t " a A " l : i c { e " " n } a } m , e " : " B o b " } } ,
Response indicates per-operation results:
1
2
3
4
5
6
7
{
"results" : [
{ "status" : "success" , "data" : { "id" : "789" }},
{ "status" : "success" , "data" : { "id" : "123" }},
{ "status" : "error" , "error" : { "code" : "NOT_FOUND" }}
]
}
HATEOAS (When It Helps)# Include links to related actions:
1
2
3
4
5
6
7
8
9
10
11
12
{
"data" : {
"id" : "123" ,
"status" : "pending" ,
"links" : {
"self" : "/orders/123" ,
"approve" : "/orders/123/approve" ,
"cancel" : "/orders/123/cancel" ,
"items" : "/orders/123/items"
}
}
}
Don’t over-engineer it. Add links that genuinely help clients navigate.
Documentation# Document every endpoint:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
## Create User
`POST /users`
Creates a new user account.
### Request
```json
{
"name": "Alice",
"email": "alice@example .com",
"role": "member"
}
Response# 201 Created
1
2
3
4
5
6
7
{
"data" : {
"id" : "123" ,
"name" : "Alice" ,
"email" : "alice@example.com"
}
}
Errors# Code Description 400 Invalid request body 409 Email already exists
G P o i o c d k A c P o I n v d e e n s t i i g o n n s i s e a a r b l o y u . t D c o o c n u s m i e s n t t e n t c h y e m a . n d E n p f r o e r d c i e c t t a h b e i m l i i t n y . c o W d h e e n r e d v e i v e e w l . o p T e h r e s A c P a I n t g h u a e t s ' s s h e o a w s y a n t o e n i d n p t o e i g n r t a t w e o r i k s s t b h e e f o A r P e I r t e h a a d t i n g g e t t s h e a d d o o p c t s e , d . y o u ' v e d o n e i t r i g h t .