Designing a REST API That Stays Maintainable
An API is a contract you will live with for years. These are the decisions that keep it understandable as the system grows.
Anyone can build an API that works today. The difficult part is building one that still makes sense in two years, when three clients depend on it and the business rules have changed twice.
Model resources, not actions
URLs describe things, not verbs. /patients/42 is a resource. /getPatient?id=42 is an action pretending to be a resource, and it will fight you every time you add a client.
GET /patients # list
POST /patients # create
GET /patients/42 # read
PATCH /patients/42 # partial update
DELETE /patients/42 # remove
Use status codes honestly
200 OK— the request succeeded201 Created— a resource was created (return its location)400 Bad Request— the client sent something invalid401 Unauthorized— not authenticated403 Forbidden— authenticated, not allowed404 Not Found— no such resource422 Unprocessable Entity— validation failed429 Too Many Requests— slow down
Returning 200 with {"error": true} forces every client to write special-case handling.
Version from the start
Put the version in the path (/api/v1/...) or a header. You will need it. Breaking changes then become additive: ship v2 while v1 keeps working until clients migrate.
Return consistent error shapes
{
"error": {
"code": "validation_failed",
"message": "The request could not be validated.",
"fields": { "email": "A valid email address is required." }
}
}
One shape means one error handler in every client.
Paginate everything that is a list
GET /patients?page=2&per_page=50
Include total counts and links. A list endpoint that returns everything will work in development and fail in production.
Make writes idempotent
Payment and order APIs must tolerate retries. Accept a client-generated idempotency key and return the original result for a repeated key instead of creating a second record.
Document as you ship
An endpoint that is not documented does not exist. Keep the documentation next to the code — generated from annotations or tests — so it cannot drift silently.
Validate on the server, always
Client-side validation is a user experience feature. Server-side validation is a security control. Never assume the client already checked.
The goal is boring: predictable resources, honest status codes, stable contracts. Boring APIs are the ones that survive.