REST APIs continue to dominate as the backbone of modern web services, but best practices evolve with new technologies and standards.
Use nouns to represent resources rather than actions in your endpoints:
# Good
GET /users
GET /users/{id}
# Bad
GET /getUsers
GET /getUserById/{id}
Consistently use plural nouns for collections:
GET /products
POST /products
GET /products/{id}
Express hierarchical relationships through path structure:
GET /users/{userId}/orders
POST /users/{userId}/orders
GET /users/{userId}/orders/{orderId}
Include the version in the URL path for clarity:
GET /v2/products
Alternatively, use custom headers for version control:
GET /products
Accept-Version: 2.0
Follow semantic versioning (SemVer) for API changes:
GET /v1.2.3/products
Implement HATEOAS to make your API self-descriptive:
{
"id": 123,
"name": "Premium Widget",
"price": 99.99,
"_links": {
"self": { "href": "/products/123" },
"reviews": { "href": "/products/123/reviews" },
"purchase": { "href": "/orders", "method": "POST" }
}
}
Use appropriate HTTP status codes:
200 OK - Successful GET requests201 Created - Resource created204 No Content - Successful DELETE400 Bad Request - Client error401 Unauthorized - Authentication needed404 Not Found - Resource doesn't exist429 Too Many Requests - Rate limiting500 Internal Server Error - Server failureProvide structured error details:
{
"error": {
"code": "invalid_parameter",
"message": "The 'price' parameter must be a positive number",
"target": "price",
"details": [
{
"code": "min_value",
"message": "Value must be greater than 0"
}
]
}
}
Use cursors for efficient pagination:
GET /products?limit=20&after=MjAyMy0wMS0wMVQwMDowMDowMF9pZDEyMw==
Response includes pagination metadata:
{
"data": [...],
"pagination": {
"next_cursor": "MjAyMy0wMS0wMVQwMDowMDowMF9pZDEyNA==",
"has_more": true
}
}
Support complex filtering with a standardized syntax:
GET /products?filter=price>100&sort=-created_at,price&fields=id,name,price
Allow clients to request only needed fields:
GET /users?fields=id,name,email
Enable response compression:
GET /products
Accept-Encoding: gzip, deflate, br
Support caching with ETags:
GET /products/123
If-None-Match: "686897696a7c876b7e"
Use OAuth 2.1 (RFC 6749bis) with PKCE:
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=xyz
&client_id=client
&code_verifier=abc
&redirect_uri=https://app/callback
Implement rate limiting headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 3600
Validate all inputs:
# Python example using Pydantic
from pydantic import BaseModel, constr, conint
class CreateUser(BaseModel):
username: constr(min_length=3, max_length=20)
email: EmailStr
age: conint(gt=0, lt=120)
Use OpenAPI 3.1 for machine-readable documentation:
openapi: 3.1.0
info:
title: Product API
version: 1.0.0
paths:
/products:
get:
summary: List all products
parameters:
- $ref: '#/components/parameters/page'
responses:
'200':
description: A list of products
content:
application/json:
schema:
$ref: '#/components/schemas/Products'
Provide developer-friendly interfaces like Swagger UI or Redoc.
Support event subscriptions:
POST /webhooks
Content-Type: application/json
{
"event": "order.created",
"callback_url": "https://client.example.com/notifications",
"secret": "client-secret"
}
For long-running operations:
POST /imports
Content-Type: application/json
{
"file_url": "https://example.com/data.csv"
}
Response:
HTTP/1.1 202 Accepted
Location: /operations/import-123
Retry-After: 30
Consider adding a GraphQL compatibility layer for flexibility:
type Query {
products(filter: ProductFilter, page: Pagination): ProductConnection
}
type Product {
id: ID!
name: String!
price: Float!
}
By following these practices, you'll create APIs that are scalable, maintainable, and developer-friendly in 2025's evolving technical landscape.