API Design Best Practices¶
REST Fundamentals¶
REST Maturity Model (Richardson)¶
Resource Naming¶
Conventions¶
Resource Examples¶
# Collections
GET /orders # List orders
POST /orders # Create order
GET /orders/123 # Get specific order
PUT /orders/123 # Replace order
PATCH /orders/123 # Partial update
DELETE /orders/123 # Delete order
# Nested resources
GET /customers/123/orders # Orders for customer 123
POST /orders/456/items # Add item to order 456
# Actions (when CRUD doesn't fit)
POST /orders/123/cancel # Cancel order
POST /orders/123/refund # Refund order
POST /accounts/123/transfer # Transfer money
# Filtering, Sorting, Pagination
GET /orders?status=pending&sort=-created_at&page=2&limit=20
# Search
GET /products/search?q=laptop&category=electronics
HTTP Methods¶
PUT vs PATCH¶
// PUT - Replace entire resource
PUT /users/123
{
"name": "John Doe",
"email": "[email protected]",
"phone": "555-1234",
"address": "123 Main St"
}
// PATCH - Partial update
PATCH /users/123
{
"email": "[email protected]"
}
// PATCH with JSON Patch (RFC 6902)
PATCH /users/123
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "/email", "value": "[email protected]" },
{ "op": "add", "path": "/phone", "value": "555-9999" }
]
HTTP Status Codes¶
Request/Response Design¶
Request Format¶
// Create order
POST /orders
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"customer_id": "cust_123",
"items": [
{
"product_id": "prod_456",
"quantity": 2
}
],
"shipping_address": {
"line1": "123 Main St",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
},
"metadata": {
"source": "mobile_app"
}
}
Response Format¶
// Success response
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/ord_789
{
"id": "ord_789",
"object": "order",
"customer_id": "cust_123",
"status": "pending",
"items": [...],
"subtotal": 5000,
"tax": 450,
"total": 5450,
"currency": "usd",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
// Error response
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "The customer_id provided is invalid.",
"param": "customer_id",
"doc_url": "https://api.example.com/docs/errors#parameter_invalid"
}
}
// Validation errors
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"type": "validation_error",
"code": "validation_failed",
"message": "Validation failed",
"errors": [
{
"field": "email",
"code": "invalid_format",
"message": "Email format is invalid"
},
{
"field": "items",
"code": "required",
"message": "At least one item is required"
}
]
}
}
Pagination¶
Offset Pagination¶
GET /orders?offset=20&limit=10
{
"data": [...],
"pagination": {
"offset": 20,
"limit": 10,
"total": 150,
"has_more": true
}
}
Pros: Simple, allows jumping to any page
Cons: Inconsistent with concurrent changes, slow for large offsets
Cursor Pagination (Recommended)¶
GET /orders?limit=10&starting_after=ord_xyz
{
"data": [...],
"has_more": true,
"cursors": {
"next": "ord_abc",
"previous": "ord_xyz"
}
}
Pros: Consistent, efficient, works with real-time data
Cons: Can't jump to arbitrary page
Page-Based Pagination¶
GET /orders?page=3&per_page=20
{
"data": [...],
"meta": {
"current_page": 3,
"per_page": 20,
"total_pages": 8,
"total_count": 150
},
"links": {
"first": "/orders?page=1",
"prev": "/orders?page=2",
"next": "/orders?page=4",
"last": "/orders?page=8"
}
}
Filtering, Sorting, and Field Selection¶
Filtering¶
# Simple equality
GET /products?category=electronics&status=active
# Comparison operators
GET /orders?created_at[gte]=2024-01-01&total[lt]=1000
GET /orders?created_at.gte=2024-01-01 # Alternative syntax
# Multiple values (OR)
GET /products?category=electronics,clothing
# Search
GET /products?q=laptop
# Nested filtering
GET /orders?customer.country=US
Sorting¶
# Single field (ascending)
GET /products?sort=price
# Descending
GET /products?sort=-price
GET /products?sort=price&order=desc # Alternative
# Multiple fields
GET /products?sort=-created_at,name
GET /products?sort=category,-price
Field Selection (Sparse Fieldsets)¶
# Select specific fields
GET /orders?fields=id,status,total
GET /users/123?fields=name,email
# Expand nested objects
GET /orders/123?expand=customer,items.product
Versioning¶
URL Path Versioning (Recommended)¶
Header Versioning¶
GET /orders
Accept: application/vnd.api+json; version=2
Pros: Clean URLs
Cons: Hidden, harder to test
Query Parameter¶
Best Practices¶
Authentication & Security¶
Authentication Methods¶
Security Headers¶
# Response headers
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
X-Request-Id: req_abc123 # For debugging/tracing
Rate Limiting¶
# Response headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1640000000
Retry-After: 60 # When rate limited
# Rate limit exceeded response
HTTP/1.1 429 Too Many Requests
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please retry after 60 seconds.",
"retry_after": 60
}
}
Rate Limiting Strategies¶
Idempotency¶
Error Handling¶
Error Response Structure¶
{
"error": {
"type": "invalid_request_error",
"code": "resource_missing",
"message": "No such customer: cust_xyz",
"param": "customer_id",
"request_id": "req_abc123",
"doc_url": "https://api.example.com/docs/errors#resource_missing"
}
}
Error Types¶
HATEOAS (Hypermedia)¶
// Order response with links
{
"id": "ord_123",
"status": "pending",
"total": 5000,
"_links": {
"self": {
"href": "/orders/ord_123"
},
"customer": {
"href": "/customers/cust_456"
},
"cancel": {
"href": "/orders/ord_123/cancel",
"method": "POST"
},
"pay": {
"href": "/orders/ord_123/pay",
"method": "POST"
}
},
"_embedded": {
"items": [
{
"product_id": "prod_789",
"quantity": 2,
"_links": {
"product": { "href": "/products/prod_789" }
}
}
]
}
}
API Documentation¶
OpenAPI (Swagger) Example¶
openapi: 3.0.0
info:
title: Orders API
version: 1.0.0
description: API for managing orders
paths:
/orders:
get:
summary: List orders
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, confirmed, shipped]
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: List of orders
content:
application/json:
schema:
$ref: '#/components/schemas/OrderList'
post:
summary: Create order
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
'400':
description: Invalid request
components:
schemas:
Order:
type: object
properties:
id:
type: string
status:
type: string
total:
type: integer
Common Interview Questions¶
- PUT vs PATCH?
- PUT: Replace entire resource (idempotent)
-
PATCH: Partial update (can be idempotent)
-
How to handle API versioning?
- URL path (recommended), header, or query param
-
Support multiple versions, deprecation policy
-
Pagination approaches?
- Offset: Simple but inconsistent
-
Cursor: Efficient, consistent (recommended)
-
How to make APIs idempotent?
- Idempotency keys for POST requests
-
Natural idempotency for PUT/DELETE
-
REST vs RPC-style APIs?
- REST: Resource-oriented, HTTP semantics
- RPC: Action-oriented, single endpoint