What you'll learn
Quick Answer
Name resources with plural nouns, use HTTP methods for the verb, return meaningful status codes, paginate every list, and keep error responses in one consistent shape. Consistency matters more than any individual choice.
URLs name things, methods do things
The single most common mistake is putting verbs in URLs:
POST /getStudent bad
POST /createStudent bad
POST /deleteStudent/5 bad
The HTTP method is already the verb. The URL should name the resource:
GET /students list
POST /students create
GET /students/5 fetch one
PUT /students/5 replace
PATCH /students/5 partial update
DELETE /students/5 delete
Use plural nouns consistently. Mixing /student/5 and /students means every developer must remember which endpoint uses which.
Nest to express relationships, but only one level: /students/5/enrolments is clear, while /students/5/enrolments/9/assignments/3/comments is not. Beyond one level, expose the resource at the top with a filter instead.
Method semantics matter
These are not arbitrary labels — clients, proxies and caches rely on them.
- GET must not change anything. A GET that deletes will eventually be triggered by a crawler or a prefetch.
- GET, PUT and DELETE are idempotent — repeating them gives the same end state. POST is not, which is why a duplicate form submission creates two records.
- PUT replaces the whole resource; PATCH updates part of it. Sending a partial body to PUT should logically blank the missing fields, which is why PATCH exists.
Idempotency matters for retries. A client that times out on a POST cannot safely retry, since it does not know whether the first attempt succeeded. Support an idempotency key header for such endpoints — see idempotency explained.
Status codes are part of the contract
Returning 200 OK with {"success": false} forces every client to parse the body to discover a failure, and breaks every tool that reasons about HTTP.
- 200 OK · 201 Created, with a
Locationheader · 204 No Content, typical for DELETE - 400 malformed or invalid · 401 not authenticated · 403 authenticated but not permitted · 404 not found
- 409 conflict, such as a duplicate · 422 well-formed but semantically invalid · 429 rate limited
- 500 server error · 503 temporarily unavailable
The 4xx versus 5xx split carries real meaning: 4xx means the client must change something, 5xx means retrying may work. Monitoring and retry logic both depend on that distinction being honest, so do not return 500 for validation failures.
Pagination, filtering and sorting
Paginate every list endpoint from the start. An endpoint returning all records works fine with 50 rows in development and falls over at 500,000 in production. Retrofitting pagination is a breaking change.
GET /students?page=2&limit=50
GET /students?cursor=eyJpZCI6MTAwfQ&limit=50
Offset pagination is simple but slows on deep pages and can skip or repeat rows when data changes mid-traversal. Cursor pagination is stable and efficient, and is the better default for large or frequently-changing datasets.
Return the metadata clients need — total count where feasible, and a next-page cursor or link.
Use query parameters for filtering and sorting, and keep them consistent: ?stream=Science&min_marks=80&sort=-marks, where the leading minus means descending. Whatever convention you choose, apply it everywhere.
One error shape, everywhere
Clients write error handling once. If every endpoint returns a differently-shaped error, they cannot.
{
"error": {
"code": "validation_failed",
"message": "marks must be between 0 and 100",
"details": [{ "field": "marks", "issue": "out_of_range" }]
}
}
A stable machine-readable code matters more than the message, because clients should branch on the code and display the message. Changing wording then never breaks anyone.
Never leak stack traces, SQL or file paths in production errors — they tell an attacker about your schema and structure.
Two closing points. Version from day one (/v1/students) so you can change later without breaking clients — see API versioning strategies. And write the documentation as you build; an API nobody can use without reading your source code is not finished.
