REST API design tips
Best practices and tips for designing RESTful APIs, including naming conventions, versioning, and endpoint structuring.
Tips
- Always have separate delete endpoint to delete a resource even for soft delete where you just set is_deleted flag to true.
- Never allow PATCH to delete resource.
- Send 204 to already deleted resource without raising exception of resource already deleted ( Idempotency principles).
Naming Conventions
- Use kebab-case for URL paths and snake_case or camelCase for query parameters.
- When using hyphens in query params, it can clash with minus‑sign semantics in some contexts. most libraries won't parse them as identifiers. Hence, avoid hyphens in query keys.
POST /sign-in POST /sign-in?redirectUrl=https://example.com/dashboard # camelCase query params when JS frameworks POST /sign_in?redirect_url=https://example.com/dashboard # snake_case query params when Python, Ruby, etc.
2. Query Parameters
You have two main, equally valid options—pick one and be consistent:
A. snake_case
- Lowercase + underscores
- Common in many REST APIs and frameworks
- Example:
GET /products?category=electronics&price_min=100&sort_by=popularity
B. camelCase
- Lowercase first word, then capital letters for subsequent words
- Aligns with many JavaScript front‑end conventions
- Example:
GET /products?category=electronics&priceMin=100&sortBy=popularity
A Note on Hyphens in Query Strings
- Hyphens can clash with minus‑sign semantics in some contexts; most libraries won't parse them as identifiers.
- For that reason, avoid hyphens in query keys.
3. General Tips
- Keep names semantic
- Use real words:
sortBy,filter,pageSize,lang
- Use real words:
- Document your choice in your API style guide so every team follows the same format.
- Versioning: if you version in the URL, do it as its own segment:
/v1/products
API endpoint should be resource not action
https://api.example.com/tasks // [!code ++]
https://api.example.com/get-tasks // [!code --]
Collection name should be plural
https://api.example.com/tasks // [!code ++]
https://api.example.com/tasks/{task_id} // [!code ++]
https://api.example.com/task // [!code --]
https://api.example.com/task/{task_id} // [!code --]
Group auth related endpoints under /auth prefix
POST /auth/register
POST /auth/login
POST /auth/verification-emails
GET /auth/verification-emails/{token}
POST /auth/forgot-password
POST /auth/reset-password
Entity CRUD endpoints conventions
GET /tasks # Get all tasks
POST /tasks # Create new task
GET /tasks/{task_id} # Get task by id
PUT /tasks/{task_id} # Update task by id (Replace)
PATCH /tasks/{task_id} # Partial update task by id
DELETE /tasks/{task_id} # Delete task by id
POST /tasks/batch # Create multiple tasks
GET /tasks/{task_id}/comments # Get all comments for task
POST /tasks/{task_id}/comments # Create new comment for task
GET /comments/{comment_id} # Get comment by id for task
PUT /comments/{comment_id} # Update comment by id for task
PATCH /comments/{comment_id} # Partial update comment by id for task
DELETE /comments/{comment_id} # Delete comment by id for task
Don't go deeper than 3 levels: collection/resource/collection
https://api.example.com/tasks/1/comments // [!code ++]
https://api.example.com/tasks/1/comments/1/replies // [!code --]
Filter, Sorting & Pagination
<!-- Filter: Use key-value pair -->
https://api.example.com/tasks?completed=true
https://api.example.com/users?last_name=john&age=36
<!-- Filter: Provide filtering on specific property/column/field with global search -->
https://api.example.com/users?q=john
https://api.example.com/tasks?q=john&fields=username,email
<!--
Pagination: Prefer using limit-offset instead of page-size
?page=1&size=10 => fine for simple pagination
🤔 => How to get 10 items starting from 4th item with page-size?
-->
https://api.example.com/tasks?limit=10&offset=0
https://api.example.com/tasks?limit=10&offset=3 <- You can get 10 items starting from 4th item with limit-offset
<!-- Sorting: sorting key with order -->
https://api.example.com/tasks?sort=-created_at,title
Instead of returning list return object with name for making your response future proof
# --- Before
[
{ "id": 1, "title": "Buy Tomato", "is_completed": False },
{ "id": 2, "title": "Learn Sanskrit", "is_completed": False },
]
# --- After
{
"tasks": [
{ "id": 1, "title": "Buy Tomato", "is_completed": False },
{ "id": 2, "title": "Learn Sanskrit", "is_completed": False },
],
"total": 2,
}
Don't return map structure in response, instead return list of objects
# --- Before
{
"john": [
{ "id": 1, "title": "Buy Tomato", "is_completed": False },
{ "id": 2, "title": "Learn Sanskrit", "is_completed": False },
],
"jane": [
{ "id": 3, "title": "Buy Apple", "is_completed": False },
{ "id": 4, "title": "Finish reading book", "is_completed": False },
]
}
# --- After
{
"tasks": [
{ "id": 1, "title": "Buy Tomato", "is_completed": False, "user": "john" },
{ "id": 2, "title": "Learn Sanskrit", "is_completed": False, "user": "john" },
{ "id": 3, "title": "Buy Apple", "is_completed": False, "user": "jane" },
{ "id": 4, "title": "Finish reading book", "is_completed": False, "user": "jane" },
],
"total": 4
}
Async/Longer Operations
- For longer processes (
POST{ processId: 99 }=>/long-process/create) Send202status code to indicate that request has been accepted for processing- Add status endpoint URL in response header
Locationto check the status of the operation
- Add status endpoint URL in response header
- Expose
GET(/long-process/status/99) endpoint to check the status of the operation- Return
200status code if operation isn't completed with any of the below response:- Provide status of the operation
{ status: "In Progress" } - Provide estimated time to complete
{ "time_to_complete_in_minutes": "15" } - You can also provide link to cancel/stop the operation
{ "rel": "cancel", "method": "delete", "href": "/long-process/cancel/99" }
- Provide status of the operation
- Return 303 status code with
Locationheader with URL to created resource once operation is completed{ "status": "completed" }
- Return
API response should be generic and agnostic
- Even if you are building API for a specific client, make sure your API response is generic and agnostic
- Don't return client specific data in response. Doing so will make your API tightly coupled with the client and requires changing both client and server code whenever requirement changes.
- E.g. For chat page, instead of single endpoint for chat details, chat messages, meta data, etc. create separate endpoints for each of them.
SEO
- Hyphens improve keyword recognition in URL (excluding query parameters). E.g.
/product-details