API Design Standards
Overview
This document outlines the design standards and principles for building consistent, maintainable, and developer-friendly APIs. All API endpoints must adhere to these standards.
RESTful Principles
Resource-Oriented Design
Design APIs around resources (nouns) rather than actions (verbs):
Good:
GET /users
POST /users
GET /users/123
PUT /users/123
DELETE /users/123Bad:
POST /getUsers
POST /createUser
POST /updateUser
POST /deleteUserHTTP Methods
Use HTTP methods correctly to indicate the action:
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resources | Yes | Yes |
| POST | Create new resources | No | No |
| PUT | Replace entire resources | Yes | No |
| PATCH | Partially update resources | No | No |
| DELETE | Remove resources | Yes | No |
Idempotency
Ensure PUT and DELETE are idempotent - calling them multiple times should have the same effect as calling them once:
// PUT - Idempotent
PUT /users/123
{
"name": "John Doe",
"email": "john@example.com"
}
// Calling this multiple times results in the same state
// POST - Not Idempotent
POST /users
{
"name": "John Doe"
}
// Calling this multiple times creates multiple usersURL Design
URL Structure
Follow this consistent URL pattern:
https://api.example.com/{version}/{resource}/{id}/{sub-resource}/{sub-id}Examples:
GET /v1/users/123
GET /v1/users/123/posts
GET /v1/users/123/posts/456
GET /v1/posts/456/commentsNaming Conventions
Use Plural Nouns
Always use plural nouns for collections:
Good:
GET /users
GET /posts
GET /commentsBad:
GET /user
GET /post
GET /commentUse Kebab-Case for URLs
Use lowercase with hyphens for multi-word resources:
Good:
GET /user-profiles
GET /blog-posts
GET /api-keysBad:
GET /userProfiles
GET /BlogPosts
GET /API_KeysAvoid Deep Nesting
Limit URL nesting to 2-3 levels:
Good:
GET /users/123/posts
GET /posts?user_id=123Bad:
GET /users/123/posts/456/comments/789/likesFor deep relationships, use query parameters or separate endpoints:
GET /comments?post_id=456
GET /likes?comment_id=789Query Parameters
Filtering
Use query parameters for filtering collections:
GET /users?status=active
GET /users?role=admin&status=active
GET /posts?published=true&category=techSearching
Use q or search parameter for full-text search:
GET /users?q=john
GET /posts?search=api+designSorting
Use sort parameter with comma-separated fields:
GET /users?sort=created_at # Ascending
GET /users?sort=-created_at # Descending (prefix with -)
GET /users?sort=-created_at,name # Multiple fieldsField Selection
Allow clients to request specific fields:
GET /users?fields=id,name,emailResponse:
{
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
]
}Pagination
Always include pagination parameters:
GET /users?page=2&per_page=20
GET /users?cursor=abc123&limit=20See Pagination for detailed guidelines.
Request Design
Request Headers
Required Headers
All requests should include:
Content-Type: application/json
Accept: application/json
Authorization: Bearer {token}Optional Headers
Accept-Language: en-US
User-Agent: MyApp/1.0
X-Request-ID: unique-request-idRequest Body
Use JSON
Always use JSON for request bodies:
POST /users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}Snake Case for Properties
Use snake_case for JSON property names:
Good:
{
"first_name": "John",
"last_name": "Doe",
"date_of_birth": "1990-01-01"
}Bad:
{
"firstName": "John",
"LastName": "Doe",
"DateOfBirth": "1990-01-01"
}Nested Objects
Structure nested objects logically:
{
"name": "John Doe",
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "Springfield",
"country": "US"
},
"preferences": {
"newsletter": true,
"theme": "dark"
}
}Arrays
Use arrays for collections:
{
"tags": ["api", "design", "rest"],
"roles": ["admin", "editor"]
}Response Design
Response Structure
Standard Success Response
Wrap data in a consistent structure:
{
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
}For collections:
{
"data": [
{ "id": 1, "name": "User 1" },
{ "id": 2, "name": "User 2" }
],
"meta": {
"total": 100,
"page": 1,
"per_page": 20
},
"links": {
"first": "/users?page=1",
"last": "/users?page=5",
"prev": null,
"next": "/users?page=2"
}
}Standard Error Response
Use a consistent error format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid.",
"details": {
"email": [
"The email field is required."
],
"password": [
"The password must be at least 8 characters."
]
}
}
}HTTP Status Codes
Use appropriate status codes:
Success Codes (2xx)
| Code | Usage |
|---|---|
| 200 OK | Successful GET, PUT, PATCH, DELETE |
| 201 Created | Successful POST that creates a resource |
| 202 Accepted | Request accepted for async processing |
| 204 No Content | Successful request with no response body |
Client Error Codes (4xx)
| Code | Usage |
|---|---|
| 400 Bad Request | Invalid request format |
| 401 Unauthorized | Authentication required or failed |
| 403 Forbidden | Authenticated but not authorized |
| 404 Not Found | Resource doesn't exist |
| 405 Method Not Allowed | HTTP method not supported |
| 409 Conflict | Resource conflict (duplicate) |
| 422 Unprocessable Entity | Validation errors |
| 429 Too Many Requests | Rate limit exceeded |
Server Error Codes (5xx)
| Code | Usage |
|---|---|
| 500 Internal Server Error | Unexpected server error |
| 502 Bad Gateway | Invalid upstream response |
| 503 Service Unavailable | Server temporarily unavailable |
| 504 Gateway Timeout | Upstream timeout |
Response Examples
Create Resource (201)
POST /users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}Response:
HTTP/1.1 201 Created
Location: /users/123
Content-Type: application/json
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-01T00:00:00Z"
}
}Update Resource (200)
PATCH /users/123
Content-Type: application/json
{
"name": "Jane Doe"
}Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"id": 123,
"name": "Jane Doe",
"email": "john@example.com",
"updated_at": "2024-01-01T12:00:00Z"
}
}Delete Resource (204)
DELETE /users/123Response:
HTTP/1.1 204 No ContentValidation Error (422)
POST /users
Content-Type: application/json
{
"email": "invalid-email"
}Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid.",
"details": {
"name": ["The name field is required."],
"email": ["The email must be a valid email address."]
}
}
}Data Types and Formats
Dates and Times
Always use ISO 8601 format in UTC:
{
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T12:30:45Z",
"published_at": "2024-01-02T08:00:00Z"
}Booleans
Use true and false (not 1/0 or "true"/"false"):
{
"is_active": true,
"is_verified": false,
"has_premium": true
}Numbers
Use appropriate numeric types:
{
"id": 123, // Integer
"price": 19.99, // Float
"quantity": 5, // Integer
"rating": 4.5 // Float
}Null Values
Include null values when a field exists but has no value:
{
"name": "John Doe",
"middle_name": null,
"bio": null
}Optionally omit null values if documented:
{
"name": "John Doe"
}Enumerations
Use consistent string values for enums:
{
"status": "active", // Not "Active" or "ACTIVE"
"role": "admin", // Not "ADMIN"
"priority": "high" // Not "HIGH"
}Document allowed values:
Status: draft | published | archived
Role: user | admin | moderator
Priority: low | medium | highVersioning
Version in URL
Include version in the URL path:
https://api.example.com/v1/users
https://api.example.com/v2/usersSee API Versioning for detailed guidelines.
Relationships
Embedded Resources
For simple relationships, embed related data:
{
"id": 1,
"title": "Blog Post",
"author": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
}Resource Links
For complex relationships, use links:
{
"id": 1,
"title": "Blog Post",
"author_id": 123,
"links": {
"author": "/users/123",
"comments": "/posts/1/comments"
}
}Include Parameter
Allow clients to request related resources:
GET /posts/1?include=author,commentsResponse:
{
"data": {
"id": 1,
"title": "Blog Post",
"author": {
"id": 123,
"name": "John Doe"
},
"comments": [
{ "id": 1, "text": "Great post!" }
]
}
}Batch Operations
Bulk Create
POST /users/batch
Content-Type: application/json
{
"users": [
{ "name": "User 1", "email": "user1@example.com" },
{ "name": "User 2", "email": "user2@example.com" }
]
}Response:
{
"data": [
{ "id": 1, "name": "User 1", "email": "user1@example.com" },
{ "id": 2, "name": "User 2", "email": "user2@example.com" }
],
"meta": {
"created": 2,
"failed": 0
}
}Bulk Update
PATCH /users/batch
Content-Type: application/json
{
"updates": [
{ "id": 1, "status": "active" },
{ "id": 2, "status": "inactive" }
]
}Bulk Delete
DELETE /users/batch
Content-Type: application/json
{
"ids": [1, 2, 3, 4, 5]
}Asynchronous Operations
For long-running operations, return 202 Accepted:
POST /exports/usersResponse:
HTTP/1.1 202 Accepted
Location: /jobs/abc123
Content-Type: application/json
{
"data": {
"job_id": "abc123",
"status": "processing",
"created_at": "2024-01-01T00:00:00Z"
},
"links": {
"status": "/jobs/abc123"
}
}Check status:
GET /jobs/abc123Response:
{
"data": {
"job_id": "abc123",
"status": "completed",
"result": {
"download_url": "/downloads/users-export.csv"
},
"completed_at": "2024-01-01T00:05:00Z"
}
}File Uploads
Single File Upload
POST /users/123/avatar
Content-Type: multipart/form-data
[Binary data]Response:
{
"data": {
"url": "https://cdn.example.com/avatars/123.jpg",
"size": 102400,
"mime_type": "image/jpeg"
}
}Multiple File Upload
POST /posts/456/attachments
Content-Type: multipart/form-data
[Multiple binary files]Response:
{
"data": [
{
"id": 1,
"url": "https://cdn.example.com/files/file1.pdf",
"name": "document.pdf"
},
{
"id": 2,
"url": "https://cdn.example.com/files/file2.jpg",
"name": "image.jpg"
}
]
}Search Endpoints
Simple Search
GET /users?q=john
GET /posts?search=api+designAdvanced Search
POST /search/users
Content-Type: application/json
{
"query": "john",
"filters": {
"status": "active",
"role": ["admin", "editor"]
},
"sort": "-created_at",
"page": 1,
"per_page": 20
}Webhooks
Webhook Payload
{
"event": "user.created",
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"timestamp": "2024-01-01T00:00:00Z",
"webhook_id": "wh_abc123"
}Webhook Headers
POST https://client.example.com/webhook
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Event: user.created
X-Webhook-ID: wh_abc123Best Practices
1. Be Consistent
- Use the same patterns across all endpoints
- Maintain consistent naming conventions
- Use the same error format everywhere
2. Be Explicit
- Clearly name endpoints and parameters
- Document all possible values
- Provide examples in documentation
3. Be Backward Compatible
- Don't remove fields without deprecation
- Add new fields as optional
- Version breaking changes
4. Be Secure
- Always validate input
- Use authentication and authorization
- Rate limit all endpoints
- Sanitize output
5. Be Efficient
- Implement pagination for all collections
- Support field selection
- Enable caching with ETags
- Use compression
6. Be Helpful
- Provide clear error messages
- Include request IDs for debugging
- Document all endpoints thoroughly
- Provide SDKs when possible
Validation
Input Validation
Validate all input data:
// Example validation rules
[
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'age' => 'nullable|integer|min:0|max:150',
'status' => 'required|in:active,inactive,pending'
]Output Validation
Use response schemas to ensure consistent output:
// User resource schema
[
'id' => 'integer',
'name' => 'string',
'email' => 'string',
'created_at' => 'datetime',
'updated_at' => 'datetime'
]Documentation
OpenAPI/Swagger
Document all endpoints using OpenAPI specification:
paths:
/users:
get:
summary: List all users
parameters:
- name: page
in: query
schema:
type: integer
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'Inline Examples
Provide examples for all endpoints:
## Create User
POST /v1/users
Example request:
{
"name": "John Doe",
"email": "john@example.com"
}
Example response:
{
"data": {
"id": 123,
"name": "John Doe"
}
}Testing
Test All Endpoints
public function test_can_create_user()
{
$response = $this->postJson('/api/v1/users', [
'name' => 'John Doe',
'email' => 'john@example.com',
]);
$response->assertStatus(201)
->assertJsonStructure([
'data' => ['id', 'name', 'email']
]);
}Test Error Cases
public function test_validation_fails_with_invalid_email()
{
$response = $this->postJson('/api/v1/users', [
'name' => 'John Doe',
'email' => 'invalid-email',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['email']);
}Related Documentation
- API Overview - General API information
- API Versioning - Versioning strategy
- Pagination - Pagination guidelines
- Error Handling - Error handling standards
- Naming Conventions - Naming standards