-
Notifications
You must be signed in to change notification settings - Fork 135
SE 11 API Design Principles
Part of the Software Engineering Principles series
An API (Application Programming Interface) is a defined contract that allows software components to communicate. An API specifies what operations are available, what inputs they expect, and what outputs they produce — without exposing how they work internally.
APIs exist at many levels:
- Class-level — the public methods of a Java class
- Library-level — the public surface of a JAR file
- Service-level — HTTP endpoints a service exposes to other systems
This article focuses primarily on HTTP/REST APIs, which are the most common form in modern web applications, but the principles apply broadly.
REST (Representational State Transfer) is an architectural style for web APIs defined by Roy Fielding in 2000. A REST API models resources and uses HTTP methods to manipulate them.
| Constraint | Meaning |
|---|---|
| Stateless | Each request contains all information needed; the server stores no session state per client |
| Client-Server | UI concerns and data concerns are separated |
| Cacheable | Responses must declare whether they can be cached |
| Uniform Interface | Resources are identified by URIs; manipulation is through representations |
| Layered System | Clients do not know whether they are connected directly or through intermediaries |
| Code on Demand (optional) | Servers may send executable code to clients |
REST models things (resources), not operations. URLs should be nouns, not verbs.
// Bad — RPC-style, action-oriented
POST /createPatient
POST /getPatientById?id=42
POST /deletePatient?id=42
// Good — REST-style, resource-oriented
POST /patients (create)
GET /patients/42 (read)
PUT /patients/42 (replace)
PATCH /patients/42 (partial update)
DELETE /patients/42 (delete)
/patients — collection of all patients
/patients/42 — a specific patient
/patients/42/bills — bills belonging to patient 42
/departments/5/stock — stock in department 5
/departments/5/stock/item-12 — a specific stock item in department 5
/prescriptions/99/items — items in prescription 99
Avoid going deeper than 2–3 levels — deeply nested URIs become unwieldy.
| Method | Purpose | Safe? | Idempotent? |
|---|---|---|---|
| GET | Retrieve a resource | Yes | Yes |
| POST | Create a new resource | No | No |
| PUT | Replace a resource entirely | No | Yes |
| PATCH | Partially update a resource | No | No |
| DELETE | Remove a resource | No | Yes |
Safe: The operation has no side effects (no data modification).
Idempotent: Calling the operation multiple times produces the same result as calling it once. PUT and DELETE are idempotent: deleting an already-deleted resource should return success (or 404), not an error.
Status codes tell the caller exactly what happened. Use them precisely.
| Code | Meaning | When to use |
|---|---|---|
| 200 OK | Success | GET, PUT, PATCH responses |
| 201 Created | Resource created | POST that creates a resource |
| 204 No Content | Success with no body | DELETE, or PUT/PATCH with no response needed |
| Code | Meaning | When to use |
|---|---|---|
| 400 Bad Request | Invalid request syntax or data | Validation failures |
| 401 Unauthorized | Not authenticated | Missing or invalid credentials |
| 403 Forbidden | Authenticated but not authorised | Valid user, insufficient privileges |
| 404 Not Found | Resource does not exist | Patient 42 does not exist |
| 409 Conflict | Request conflicts with current state | Duplicate patient, stock already finalised |
| 422 Unprocessable Entity | Valid syntax but semantic errors | Business rule violation |
| Code | Meaning | When to use |
|---|---|---|
| 500 Internal Server Error | Unexpected failure | Unhandled exception |
| 503 Service Unavailable | Temporarily unable to handle requests | During maintenance or overload |
Adopt a consistent envelope for all responses:
// Success
{
"success": true,
"data": {
"id": 42,
"name": "Alice Perera",
"dateOfBirth": "1985-03-12"
}
}
// Error
{
"success": false,
"error": {
"code": "PATIENT_NOT_FOUND",
"message": "No patient found with id 42"
}
}Use camelCase for JSON field names (consistent with JavaScript conventions):
{
"patientId": 42,
"dateOfBirth": "1985-03-12",
"isActive": true,
"admissionCount": 3
}Always use ISO 8601 format. Always include the timezone offset.
{
"admittedAt": "2025-06-08T14:30:00+05:30",
"dateOfBirth": "1985-03-12"
}APIs change over time. Versioning allows existing clients to continue working while new clients use the improved version.
/api/v1/patients
/api/v2/patients
GET /api/patients
Accept: application/vnd.hmis.v2+json
- Never make breaking changes to an existing version
- A breaking change is anything that causes existing clients to fail: removing a field, changing a field type, changing a status code, removing an endpoint
- Adding new optional fields is backward-compatible (not a breaking change)
- Deprecate old versions before removing them; communicate timelines clearly
An undocumented API is unusable. Good API documentation includes:
- Overview — what the API does and who it is for
- Authentication — how to obtain and use credentials
- Endpoints — URL, method, parameters, request body, responses, error codes
- Examples — real request/response pairs
The OpenAPI Specification is the standard for documenting REST APIs. It produces machine-readable documentation that can generate client SDKs, server stubs, and interactive documentation (Swagger UI).
paths:
/patients/{id}:
get:
summary: Get patient by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Patient found
'404':
description: Patient not found| Principle | Practice |
|---|---|
| Design for the caller | Make the API easy to understand and use; hide internal complexity |
| Be consistent | Same patterns for naming, errors, and formats throughout |
| Be explicit | Use precise HTTP methods and status codes |
| Fail clearly | Error messages must tell the caller what went wrong and how to fix it |
| Version from day one | Assume the API will change; plan for it |
| Document everything | If it is not documented, it does not exist for consumers |
| Validate early | Reject bad input at the boundary with a clear error, not deep inside processing |
| Make it predictable | Callers should be able to infer how untried endpoints work from the patterns they know |
Previous: SE-10: Version Control and Git Workflows
Next: SE-12: Security Principles