URL Design
- Use kebab-case for URL path segments:
/user-profiles, not /userProfiles or /user_profiles
- Use plural nouns for resource collections:
/orders, /products, /users
- Nest resources at most one level deep:
/orders/{id}/items is fine; /orders/{id}/items/{id}/notes is too deep - promote notes to a top-level resource
- Never use verbs in URLs. The HTTP method is the verb:
DELETE /sessions/{id} not POST /logout
- Resource IDs go in the path; filters go in query params:
GET /products?category=books&min_price=10
HTTP Methods
| Method |
Semantics |
Idempotent |
Safe |
| GET |
Read resource(s) |
Yes |
Yes |
| POST |
Create resource or trigger action |
No |
No |
| PUT |
Replace resource entirely |
Yes |
No |
| PATCH |
Partial update |
No |
No |
| DELETE |
Remove resource |
Yes |
No |
- Use
POST /resources to create. Return 201 Created with Location header pointing to the new resource.
- Use
PATCH for partial updates, not PUT, unless clients always send the full representation.
Response Format
All responses use JSON. Property names use camelCase:
{
"id": "ord_01HXZ",
"userId": "usr_abc",
"status": "pending",
"createdAt": "2025-03-15T10:00:00Z",
"items": [
{ "productId": "prd_xyz", "quantity": 2, "unitPrice": 9.99 }
]
}
- Dates and times are ISO 8601 in UTC:
"2025-03-15T10:00:00Z"
- IDs are strings (never expose raw database integer IDs)
- Monetary amounts are integers in the smallest currency unit (cents), with a separate
currency field
- Booleans use true/false (not 0/1 or "yes"/"no")
- Omit null fields by default; include them only when the absence is meaningful
Error Responses
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{ "field": "email", "issue": "Must be a valid email address" }
],
"requestId": "req_01HXZ"
}
}
- Use a machine-readable
code (snake_case string) for programmatic handling
- Use a human-readable
message for display
- Always include
requestId for support and tracing
HTTP Status Codes
| Code |
When to use |
| 200 |
Successful GET, PATCH, or DELETE with body |
| 201 |
Successful POST that created a resource |
| 204 |
Successful DELETE or action with no response body |
| 400 |
Client sent an invalid request (validation errors) |
| 401 |
Authentication required or invalid credentials |
| 403 |
Authenticated but not authorized for this resource |
| 404 |
Resource not found (or deliberately hidden) |
| 409 |
Conflict (duplicate, optimistic lock failure) |
| 422 |
Request is valid JSON but semantically wrong |
| 429 |
Rate limit exceeded |
| 500 |
Unexpected server error |
Never return 200 with an error body.
Pagination
Use cursor-based pagination for all list endpoints returning potentially large data:
GET /orders?limit=25&cursor=eyJpZCI6IjEwMCJ9
Response:
{
"data": [ ... ],
"pagination": {
"limit": 25,
"nextCursor": "eyJpZCI6IjEyNSJ9",
"hasMore": true
}
}
- Default page size: 25. Maximum: 100.
- Use offset pagination only for admin UIs where jumping to a page is required.
Versioning
- Version in the URL path:
/v1/orders, /v2/orders
- Increment the major version only for breaking changes (removed fields, changed semantics)
- Deprecate old versions with a
Deprecation response header; support them for at least 12 months
- New optional fields added to responses are non-breaking and do not require a version bump
Authentication
- Use
Authorization: Bearer <token> for API keys and JWT tokens
- Never put tokens in URL query parameters (they appear in server logs)
- For API keys: prefix them with a product identifier for easy detection:
sk_live_..., pk_test_...
- Use
WWW-Authenticate header in 401 responses
Filtering, Sorting, and Searching
- Filters:
GET /products?status=active&category=books
- Sorting:
GET /orders?sort=createdAt&order=desc (default to asc)
- Full-text search:
GET /products?q=wireless+headphones
- Range filters:
GET /orders?createdAfter=2025-01-01&createdBefore=2025-03-01
Idempotency
- Accept an
Idempotency-Key header on POST requests that create resources or trigger payments
- Store the key and return the same response if the same key is replayed within 24 hours
- Return
409 Conflict if the same key is used with a different request body
Rate Limiting
Always return these headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 487
X-RateLimit-Reset: 1741694400
Retry-After: 30 (only on 429)
1---2name: api-conventions3description: REST API design conventions and standards. Apply when writing, reviewing, or discussing API endpoints, routes, controllers, serializers, or HTTP handlers. Covers URL structure, HTTP methods, response formats, error handling, pagination, versioning, and authentication headers.4---5
6## URL Design
7
8- Use kebab-case for URL path segments: `/user-profiles`, not `/userProfiles` or `/user_profiles`
9- Use plural nouns for resource collections: `/orders`, `/products`, `/users`
10- Nest resources at most one level deep: `/orders/{id}/items` is fine; `/orders/{id}/items/{id}/notes` is too deep - promote `notes` to a top-level resource
11- Never use verbs in URLs. The HTTP method is the verb: `DELETE /sessions/{id}` not `POST /logout`
12- Resource IDs go in the path; filters go in query params: `GET /products?category=books&min_price=10`
13
14## HTTP Methods
15
16| Method | Semantics | Idempotent | Safe |
17|--------|-----------|------------|------|
18| GET | Read resource(s) | Yes | Yes |
19| POST | Create resource or trigger action | No | No |
20| PUT | Replace resource entirely | Yes | No |
21| PATCH | Partial update | No | No |
22| DELETE | Remove resource | Yes | No |
23
24- Use `POST /resources` to create. Return `201 Created` with `Location` header pointing to the new resource.
25- Use `PATCH` for partial updates, not `PUT`, unless clients always send the full representation.
26
27## Response Format
28
29All responses use JSON. Property names use camelCase:
30
31```json
32{
33 "id": "ord_01HXZ",
34 "userId": "usr_abc",
35 "status": "pending",
36 "createdAt": "2025-03-15T10:00:00Z",
37 "items": [
38 { "productId": "prd_xyz", "quantity": 2, "unitPrice": 9.99 }
39 ]
40}
41```
42
43- Dates and times are ISO 8601 in UTC: `"2025-03-15T10:00:00Z"`
44- IDs are strings (never expose raw database integer IDs)
45- Monetary amounts are integers in the smallest currency unit (cents), with a separate `currency` field
46- Booleans use true/false (not 0/1 or "yes"/"no")
47- Omit null fields by default; include them only when the absence is meaningful
48
49## Error Responses
50
51```json
52{
53 "error": {
54 "code": "validation_error",
55 "message": "Request validation failed",
56 "details": [
57 { "field": "email", "issue": "Must be a valid email address" }
58 ],
59 "requestId": "req_01HXZ"
60 }
61}
62```
63
64- Use a machine-readable `code` (snake_case string) for programmatic handling
65- Use a human-readable `message` for display
66- Always include `requestId` for support and tracing
67
68## HTTP Status Codes
69
70| Code | When to use |
71|------|-------------|
72| 200 | Successful GET, PATCH, or DELETE with body |
73| 201 | Successful POST that created a resource |
74| 204 | Successful DELETE or action with no response body |
75| 400 | Client sent an invalid request (validation errors) |
76| 401 | Authentication required or invalid credentials |
77| 403 | Authenticated but not authorized for this resource |
78| 404 | Resource not found (or deliberately hidden) |
79| 409 | Conflict (duplicate, optimistic lock failure) |
80| 422 | Request is valid JSON but semantically wrong |
81| 429 | Rate limit exceeded |
82| 500 | Unexpected server error |
83
84Never return 200 with an error body.
85
86## Pagination
87
88Use cursor-based pagination for all list endpoints returning potentially large data:
89
90```
91GET /orders?limit=25&cursor=eyJpZCI6IjEwMCJ9
92```
93
94Response:
95```json
96{
97 "data": [ ... ],
98 "pagination": {
99 "limit": 25,
100 "nextCursor": "eyJpZCI6IjEyNSJ9",
101 "hasMore": true
102 }
103}
104```
105
106- Default page size: 25. Maximum: 100.
107- Use offset pagination only for admin UIs where jumping to a page is required.
108
109## Versioning
110
111- Version in the URL path: `/v1/orders`, `/v2/orders`
112- Increment the major version only for breaking changes (removed fields, changed semantics)
113- Deprecate old versions with a `Deprecation` response header; support them for at least 12 months
114- New optional fields added to responses are non-breaking and do not require a version bump
115
116## Authentication
117
118- Use `Authorization: Bearer <token>` for API keys and JWT tokens
119- Never put tokens in URL query parameters (they appear in server logs)
120- For API keys: prefix them with a product identifier for easy detection: `sk_live_...`, `pk_test_...`
121- Use `WWW-Authenticate` header in 401 responses
122
123## Filtering, Sorting, and Searching
124
125- Filters: `GET /products?status=active&category=books`
126- Sorting: `GET /orders?sort=createdAt&order=desc` (default to `asc`)
127- Full-text search: `GET /products?q=wireless+headphones`
128- Range filters: `GET /orders?createdAfter=2025-01-01&createdBefore=2025-03-01`
129
130## Idempotency
131
132- Accept an `Idempotency-Key` header on POST requests that create resources or trigger payments
133- Store the key and return the same response if the same key is replayed within 24 hours
134- Return `409 Conflict` if the same key is used with a different request body
135
136## Rate Limiting
137
138Always return these headers:
139```
140X-RateLimit-Limit: 1000
141X-RateLimit-Remaining: 487
142X-RateLimit-Reset: 1741694400
143Retry-After: 30 (only on 429)
144```