What Makes an API a Pleasure to Use?
We've all used APIs that made us want to throw our laptop. Inconsistent naming, wrong status codes, no versioning, cryptic error messages. And we've all used APIs that felt obvious and effortless. The difference is almost entirely in the design decisions made before writing the first endpoint.
This guide covers the practical REST API design principles that are worth internalizing. These aren't academic — they're the decisions that determine whether integrating your API takes 20 minutes or 2 days.
1. Resource Naming: Nouns, Not Verbs
REST is resource-oriented. Your URLs should represent things (nouns), not actions (verbs). The HTTP method expresses the action.
# Wrong — verb-based URLs
GET /getUsers
POST /createUser
PUT /updateUser/123
DELETE /deleteUser/123
# Correct — noun-based URLs
GET /users # List all users
POST /users # Create a user
GET /users/123 # Get user 123
PUT /users/123 # Replace user 123
PATCH /users/123 # Partially update user 123
DELETE /users/123 # Delete user 123
Use plural nouns consistently (/users, not /user). For nested resources, express the relationship in the URL:
GET /users/123/posts # Posts by user 123
GET /users/123/posts/456 # Specific post by user 123
2. Use HTTP Status Codes Correctly
Returning 200 OK with {"error": "User not found"} in the body is a widespread anti-pattern. HTTP status codes carry semantic meaning — use them:
- 200 OK — successful GET, PUT, PATCH
- 201 Created — successful POST that created a resource (include
Locationheader) - 204 No Content — successful DELETE or action with no response body
- 400 Bad Request — invalid input, validation failure
- 401 Unauthorized — not authenticated (missing or invalid token)
- 403 Forbidden — authenticated but not authorized for this action
- 404 Not Found — resource doesn't exist
- 409 Conflict — duplicate resource, optimistic locking failure
- 422 Unprocessable Entity — request is well-formed but semantically invalid
- 429 Too Many Requests — rate limit exceeded
- 500 Internal Server Error — something broke on your end
3. Versioning Your API
APIs evolve. Endpoints get renamed, fields get removed, behaviour changes. Without versioning, any change risks breaking existing integrations. The most common approaches:
- URL versioning (
/api/v1/users) — explicit, easy to test in a browser, easy to route at the infrastructure level. Most widely used. - Header versioning (
Accept: application/vnd.api+json; version=1) — keeps URLs clean but harder to test and document.
URL versioning is recommended for public APIs due to its simplicity. Increment the major version (v2) only for breaking changes — adding new optional fields or endpoints is non-breaking and doesn't require a new version.
4. Authentication with JWT
JWTs are the dominant authentication mechanism for REST APIs. The client authenticates once, receives a signed token, and includes it in subsequent requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Key implementation details:
- Set a short expiry (
exp) — 15 minutes to 1 hour for access tokens - Use refresh tokens with longer expiry for seamless re-authentication
- Sign with a strong secret (256-bit random key minimum)
- Validate the token on every request — never trust the payload without verifying the signature
When debugging JWT issues during development, paste the token into the JWT Decoder to inspect the header and payload claims instantly.
5. Pagination, Filtering and Sorting
Never return unbounded lists. An endpoint that returns all 500,000 users will eventually crash your database and time out for clients. Standard patterns:
# Cursor-based pagination (preferred for large datasets)
GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
# Offset pagination (simpler, fine for smaller datasets)
GET /users?page=3&per_page=20
# Filtering and sorting
GET /users?status=active&sort=created_at&order=desc
# Response should include pagination metadata
{
"data": [...],
"pagination": {
"total": 1250,
"page": 3,
"per_page": 20,
"next_cursor": "eyJpZCI6MTQzfQ"
}
}
6. Write Useful Error Responses
Your error responses are part of your API's interface. A bare 400 Bad Request with no body forces developers to guess what went wrong. A good error response tells them exactly what failed and how to fix it:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "age", "message": "must be a positive integer" }
]
}
}
Design for Your Consumers
The best test of an API design is how long it takes a new developer to make their first successful request without asking for help. Consistent naming, correct status codes, useful errors, and good documentation reduce that time dramatically. Format your JSON payloads with a tool like the JSON Formatter when writing documentation examples — well-indented, readable JSON makes a huge difference in how approachable your API feels.