API Design Patterns & Anti-Patterns¶
A comprehensive guide based on "Patterns for API Design: Simplifying Integration with Loosely Coupled Message Exchanges" by Olaf Zimmermann, Mirko Stocker, Daniel Lübke, Uwe Zdun, and Cesare Pautasso, combined with Big Tech industry standards from Google, Microsoft, AWS, Stripe, and others.
Table of Contents¶
- Foundation Patterns
- Responsibility Patterns
- Structure Patterns
- Quality Patterns
- Evolution Patterns
- Big Tech API Standards
- Anti-Patterns
- Best Practices Checklist
1. Foundation Patterns¶
1.1 API Description¶
Intent: Provide machine-readable and human-readable documentation of API capabilities.
Problem: How can API providers describe the capabilities of their APIs so that clients can discover and use them?
Solution: Create comprehensive API descriptions using: - OpenAPI/Swagger for REST APIs - Protocol Buffers for gRPC - GraphQL SDL for GraphQL APIs - AsyncAPI for event-driven APIs
# OpenAPI Example
openapi: 3.0.3
info:
title: User Service API
version: 1.0.0
description: |
Service for managing user accounts.
## Rate Limits
- 1000 requests/minute for authenticated users
- 100 requests/minute for anonymous users
paths:
/users/{id}:
get:
operationId: getUser
summary: Retrieve a user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
Big Tech Example (Google):
// Google uses Protocol Buffers with extensive comments
service LibraryService {
// Gets a book from the library.
// Returns NOT_FOUND if the book does not exist.
rpc GetBook(GetBookRequest) returns (Book) {
option (google.api.http) = {
get: "/v1/{name=publishers/*/books/*}"
};
}
}
1.2 Version Identifier¶
Intent: Communicate API version to enable evolution without breaking clients.
Problem: How can APIs evolve while maintaining backward compatibility?
Solution: Include explicit version identifiers in API contracts.
Versioning Strategies:
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL Path | /v1/users |
Clear, cacheable | URL pollution |
| Query Parameter | /users?version=1 |
Flexible | Easy to forget |
| Header | API-Version: 1 |
Clean URLs | Less visible |
| Content Negotiation | Accept: application/vnd.api+json;version=1 |
RESTful | Complex |
Google Standard:
// URL-based versioning with major version only
GET https://library.googleapis.com/v1/publishers/123/books
// Version in resource name for fine-grained control
name: "projects/my-project/locations/us-central1/datasets/my-dataset"
Stripe Standard:
# Header-based with dated versions
curl https://api.stripe.com/v1/charges \
-H "Stripe-Version: 2023-10-16"
Microsoft Standard:
1.3 Semantic Versioning for APIs¶
Pattern: Use MAJOR.MINOR.PATCH versioning: - MAJOR: Breaking changes - MINOR: Backward-compatible additions - PATCH: Backward-compatible fixes
{
"api_version": "2.3.1",
"deprecation_notice": {
"deprecated_fields": ["legacy_id"],
"sunset_date": "2024-06-01",
"migration_guide": "https://docs.example.com/migration/v3"
}
}
2. Responsibility Patterns¶
2.1 Processing Resource¶
Intent: Expose API endpoints that perform computations or trigger actions.
Problem: How can APIs expose processing capabilities that go beyond CRUD operations?
Solution: Create resources that represent actions or computations.
# Anti-pattern: Verb in URL
POST /api/calculateTax
# Pattern: Resource representing the computation
POST /api/tax-calculations
Content-Type: application/json
{
"items": [...],
"shipping_address": {...}
}
# Response includes computation result
{
"id": "calc_abc123",
"total_tax": 45.67,
"breakdown": [...]
}
Google Standard - Custom Methods:
# Use colon for custom actions
POST /v1/projects/123/instances/456:start
POST /v1/users/123:undelete
POST /v1/documents/456:batchGet
2.2 Information Holder Resource¶
Intent: Expose data entities through standard CRUD operations.
Categories:
| Type | Description | Example |
|---|---|---|
| Master Data Holder | Core business entities | /customers, /products |
| Operational Data Holder | Transactional data | /orders, /payments |
| Reference Data Holder | Lookup/config data | /countries, /currencies |
# Master Data - Customer
GET /api/v1/customers/cust_123
{
"id": "cust_123",
"type": "master_data",
"name": "Acme Corp",
"created_at": "2023-01-15T10:30:00Z",
"updated_at": "2023-06-20T14:22:00Z"
}
# Operational Data - Order
GET /api/v1/orders/ord_456
{
"id": "ord_456",
"type": "operational_data",
"customer_id": "cust_123",
"status": "processing",
"items": [...],
"created_at": "2023-06-25T09:00:00Z"
}
# Reference Data - Country
GET /api/v1/reference/countries/US
{
"code": "US",
"type": "reference_data",
"name": "United States",
"currency": "USD",
"cacheable_until": "2024-01-01T00:00:00Z"
}
2.3 Link Lookup Resource (HATEOAS)¶
Intent: Provide hypermedia links to guide clients through API capabilities.
Problem: How can clients discover related resources and available actions?
Solution: Include links in responses that describe possible transitions.
{
"id": "order_123",
"status": "pending_payment",
"total": 99.99,
"_links": {
"self": {
"href": "/api/v1/orders/order_123"
},
"customer": {
"href": "/api/v1/customers/cust_456"
},
"pay": {
"href": "/api/v1/orders/order_123/payments",
"method": "POST",
"title": "Submit payment for this order"
},
"cancel": {
"href": "/api/v1/orders/order_123",
"method": "DELETE",
"title": "Cancel this order"
}
},
"_embedded": {
"items": [...]
}
}
GitHub API Example:
{
"id": 1,
"name": "octocat/Hello-World",
"url": "https://api.github.com/repos/octocat/Hello-World",
"html_url": "https://github.com/octocat/Hello-World",
"pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
"issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}"
}
3. Structure Patterns¶
3.1 Atomic Parameter¶
Intent: Use simple, self-contained values as parameters.
Problem: How should simple data be represented in API requests and responses?
Solution: Use primitive types with clear semantics.
{
"user_id": "usr_abc123",
"email": "user@example.com",
"age": 28,
"is_active": true,
"balance": 1250.50,
"created_at": "2023-06-15T10:30:00Z"
}
Type Guidelines:
| Data Type | Format | Example |
|---|---|---|
| Identifiers | String (prefixed) | "usr_abc123", "ord_xyz789" |
| Timestamps | ISO 8601 | "2023-06-15T10:30:00Z" |
| Money | Object with amount + currency | {"amount": 1000, "currency": "USD"} |
| Duration | ISO 8601 duration | "P1DT2H30M" |
| Enums | Uppercase snake_case | "PENDING", "IN_PROGRESS" |
3.2 Parameter Tree (Nested Structures)¶
Intent: Organize related parameters into hierarchical structures.
Problem: How should complex, nested data be represented?
Solution: Use nested objects with clear hierarchy.
{
"order": {
"id": "ord_123",
"customer": {
"id": "cust_456",
"name": "John Doe",
"shipping_address": {
"line1": "123 Main St",
"line2": "Apt 4B",
"city": "San Francisco",
"state": "CA",
"postal_code": "94102",
"country": "US"
}
},
"items": [
{
"product_id": "prod_789",
"quantity": 2,
"unit_price": {
"amount": 2999,
"currency": "USD"
}
}
]
}
}
Flattening for Query Parameters:
# Nested structure in query params using dot notation
GET /api/search?filter.status=active&filter.created_at.gte=2023-01-01&sort.field=name&sort.order=asc
3.3 Parameter Forest (Collections)¶
Intent: Handle collections of items consistently.
Solution: Use arrays with consistent item structure.
{
"data": [
{"id": "1", "name": "Item 1"},
{"id": "2", "name": "Item 2"}
],
"meta": {
"total_count": 150,
"page": 1,
"per_page": 20
}
}
3.4 Pagination Patterns¶
Intent: Handle large datasets efficiently.
Offset Pagination¶
GET /api/v1/users?offset=100&limit=20
Response:
{
"data": [...],
"pagination": {
"offset": 100,
"limit": 20,
"total": 1543
}
}
Cursor Pagination (Preferred for large datasets)¶
GET /api/v1/events?cursor=eyJpZCI6MTAwfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true
}
}
Stripe Standard:
{
"object": "list",
"url": "/v1/customers",
"has_more": true,
"data": [...],
"next_page": "https://api.stripe.com/v1/customers?starting_after=cus_123"
}
Google Standard:
Keyset Pagination (Best performance)¶
GET /api/v1/logs?after_id=log_abc&limit=100
Response:
{
"data": [...],
"pagination": {
"first_id": "log_def",
"last_id": "log_xyz",
"has_more": true
}
}
3.5 Embedded Entity vs Linked Entity¶
When to Embed: - Data is always needed together - Reduces round trips - Small, bounded data
When to Link: - Large related resources - Data that changes independently - Circular references
{
"order": {
"id": "ord_123",
"customer": {
"id": "cust_456",
"name": "John Doe"
},
"customer_url": "/api/v1/customers/cust_456",
"line_items": [
{
"product_id": "prod_789",
"product_url": "/api/v1/products/prod_789",
"quantity": 2
}
]
}
}
4. Quality Patterns¶
4.1 Conditional Request¶
Intent: Optimize bandwidth and prevent conflicts using HTTP conditional headers.
For Caching (GET requests):
# First request
GET /api/v1/products/123
Response:
HTTP/1.1 200 OK
ETag: "abc123"
Last-Modified: Wed, 21 Jun 2023 07:28:00 GMT
Cache-Control: max-age=3600
# Subsequent request
GET /api/v1/products/123
If-None-Match: "abc123"
Response:
HTTP/1.1 304 Not Modified
For Optimistic Locking (PUT/PATCH requests):
PUT /api/v1/products/123
If-Match: "abc123"
Content-Type: application/json
{
"name": "Updated Product",
"price": 29.99
}
# If resource changed:
HTTP/1.1 412 Precondition Failed
{
"error": "conflict",
"message": "Resource was modified. Fetch latest version and retry.",
"current_etag": "def456"
}
4.2 Request Bundle (Batch Operations)¶
Intent: Reduce network overhead by combining multiple operations.
Google Batch API:
POST /batch
Content-Type: multipart/mixed; boundary=batch_abc123
--batch_abc123
Content-Type: application/http
Content-ID: <item1>
GET /api/v1/users/1
--batch_abc123
Content-Type: application/http
Content-ID: <item2>
GET /api/v1/users/2
--batch_abc123--
JSON Batch Pattern:
POST /api/v1/batch
Content-Type: application/json
{
"requests": [
{
"id": "req1",
"method": "GET",
"path": "/users/1"
},
{
"id": "req2",
"method": "POST",
"path": "/orders",
"body": {"customer_id": "1", "items": [...]}
}
]
}
Response:
{
"responses": [
{"id": "req1", "status": 200, "body": {...}},
{"id": "req2", "status": 201, "body": {...}}
]
}
4.3 Wish List / Sparse Fieldset¶
Intent: Allow clients to request only the fields they need.
Google Standard (Field Masks):
GET /api/v1/users/123?fields=id,name,email
# For updates - only update specified fields
PATCH /api/v1/users/123?updateMask=name,email
{
"name": "New Name",
"email": "new@example.com"
}
GraphQL Approach:
JSON:API Sparse Fieldsets:
4.4 Wish Template / Expansion¶
Intent: Allow clients to request embedded related resources.
GET /api/v1/orders/123?expand=customer,line_items.product
Response:
{
"id": "ord_123",
"customer": {
"id": "cust_456",
"name": "John Doe",
"email": "john@example.com"
},
"line_items": [
{
"id": "li_1",
"product": {
"id": "prod_789",
"name": "Widget",
"price": 29.99
},
"quantity": 2
}
]
}
Stripe Expansion:
4.5 Rate Limiting¶
Intent: Protect APIs from abuse and ensure fair usage.
Headers Standard:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1623456789
X-RateLimit-Reset-After: 3600
# When rate limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 60 seconds.",
"retry_after": 60
}
}
GitHub Rate Limiting:
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Used: 1
X-RateLimit-Reset: 1623456789
X-RateLimit-Resource: core
4.6 Error Reporting¶
Intent: Provide actionable error information.
RFC 7807 Problem Details:
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 422,
"detail": "Account balance of $10.00 is less than required $25.00",
"instance": "/api/v1/transfers/txn_abc123",
"balance": {
"current": 1000,
"required": 2500,
"currency": "USD"
},
"trace_id": "abc123xyz"
}
Google Error Standard:
{
"error": {
"code": 400,
"message": "Invalid argument",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"field": "email",
"description": "Invalid email format"
}
]
}
]
}
}
Stripe Error Standard:
{
"error": {
"type": "card_error",
"code": "card_declined",
"message": "Your card was declined.",
"param": "card_number",
"decline_code": "insufficient_funds",
"doc_url": "https://stripe.com/docs/error-codes/card-declined"
}
}
4.7 Context Representation (Idempotency)¶
Intent: Enable safe retries for non-idempotent operations.
POST /api/v1/payments
Idempotency-Key: unique-request-id-abc123
Content-Type: application/json
{
"amount": 1000,
"currency": "USD",
"customer_id": "cust_456"
}
# Retry with same key returns cached response
POST /api/v1/payments
Idempotency-Key: unique-request-id-abc123
Response:
HTTP/1.1 200 OK
Idempotency-Replayed: true
{
"id": "pay_789",
"amount": 1000,
"status": "succeeded"
}
Stripe Idempotency:
curl https://api.stripe.com/v1/charges \
-H "Idempotency-Key: $(uuidgen)" \
-d amount=2000 \
-d currency=usd
5. Evolution Patterns¶
5.1 Two in Production¶
Intent: Run multiple API versions simultaneously during migration.
┌─────────────────────────────────────────────────────┐
│ API Gateway │
├─────────────────────────────────────────────────────┤
│ /v1/* → Legacy Service (maintenance mode) │
│ /v2/* → New Service (active development) │
└─────────────────────────────────────────────────────┘
Deprecation Timeline:
# Response headers for deprecated endpoints
HTTP/1.1 200 OK
Deprecation: Sun, 01 Jan 2024 00:00:00 GMT
Sunset: Sun, 01 Jul 2024 00:00:00 GMT
Link: </api/v2/users>; rel="successor-version"
5.2 Experimental Preview¶
Intent: Release new features for early feedback before commitment.
# Opt-in to experimental features
GET /api/v1/users
X-API-Preview: new-pagination
# Or via query parameter
GET /api/v1/users?preview=new-pagination
GitHub Previews:
curl -H "Accept: application/vnd.github.scarlet-witch-preview+json" \
https://api.github.com/repos/owner/repo
5.3 Aggressive Obsolescence¶
Intent: Clearly communicate and enforce deprecation timelines.
{
"data": {...},
"_warnings": [
{
"code": "deprecated_field",
"message": "Field 'legacy_id' is deprecated. Use 'id' instead.",
"deprecated_at": "2023-01-01",
"sunset_at": "2024-01-01",
"migration": "https://docs.example.com/migrate-legacy-id"
}
]
}
6. Big Tech API Standards¶
6.1 Google API Design Guide¶
Core Principles:
1. Resource-Oriented Design: APIs are modeled as resource hierarchies
2. Standard Methods: Use standard HTTP methods (GET, POST, PUT, DELETE)
3. Custom Methods: Use POST with :action suffix for non-CRUD operations
Resource Naming:
// Collection: publishers
// Resource: publishers/{publisher}
// Sub-collection: publishers/{publisher}/books
// Sub-resource: publishers/{publisher}/books/{book}
GET /v1/publishers/123/books/456
Standard Methods Mapping:
| Method | HTTP | URI | Description |
|---|---|---|---|
| List | GET | /resources | List all resources |
| Get | GET | /resources/{id} | Get a resource |
| Create | POST | /resources | Create a resource |
| Update | PUT/PATCH | /resources/{id} | Update a resource |
| Delete | DELETE | /resources/{id} | Delete a resource |
Field Naming:
{
"display_name": "My Book",
"create_time": "2023-06-15T10:30:00Z",
"update_time": "2023-06-20T14:22:00Z",
"delete_time": null,
"expire_time": "2024-06-15T10:30:00Z",
"page_size": 20,
"page_token": "abc123"
}
6.2 Microsoft REST API Guidelines¶
Core Principles: 1. Consistency: APIs should be consistent and predictable 2. Simplicity: Simple things should be simple, complex things possible 3. Evolvability: APIs should evolve without breaking clients
URL Structure:
Response Format:
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",
"@odata.count": 50,
"@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$skiptoken=abc",
"value": [
{
"id": "user-id",
"displayName": "John Doe",
"mail": "john@example.com"
}
]
}
Error Format:
{
"error": {
"code": "BadRequest",
"message": "Invalid filter clause",
"target": "$filter",
"details": [],
"innererror": {
"code": "InvalidFilterClause",
"message": "Filter expression is invalid"
}
}
}
6.3 AWS API Standards¶
Naming Conventions:
# Service endpoint pattern
https://{service}.{region}.amazonaws.com
# Action-based (older style)
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Action=DescribeInstances&Version=2016-11-15
# REST-based (newer services)
GET /2015-03-31/functions/myFunction HTTP/1.1
Host: lambda.us-east-1.amazonaws.com
Request Signing (Signature V4):
Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20230615/us-east-1/s3/aws4_request,
SignedHeaders=host;x-amz-content-sha256;x-amz-date,
Signature=abc123...
Pagination:
6.4 Stripe API Standards¶
Design Principles: 1. Predictability: Consistent patterns across all endpoints 2. Explicit over implicit: Clear, verbose field names 3. Idempotency: All write operations support idempotency keys
Object Structure:
{
"id": "cus_abc123",
"object": "customer",
"created": 1623456789,
"livemode": true,
"metadata": {
"order_id": "12345"
},
"name": "John Doe",
"email": "john@example.com",
"balance": 0,
"default_source": "card_xyz789"
}
Expandable Fields:
# Request
curl https://api.stripe.com/v1/charges/ch_123?expand[]=customer
# Response - customer is expanded inline
{
"id": "ch_123",
"customer": {
"id": "cus_456",
"name": "John Doe"
}
}
Event-Driven (Webhooks):
{
"id": "evt_abc123",
"object": "event",
"api_version": "2023-10-16",
"created": 1623456789,
"type": "customer.created",
"data": {
"object": {...}
},
"livemode": true,
"pending_webhooks": 1
}
6.5 Twitter/X API Standards¶
Response Envelope:
{
"data": [{
"id": "1234567890",
"text": "Hello world!",
"created_at": "2023-06-15T10:30:00.000Z"
}],
"includes": {
"users": [{
"id": "user_123",
"name": "John Doe",
"username": "johndoe"
}]
},
"meta": {
"result_count": 10,
"next_token": "abc123"
}
}
Field Selection:
GET /2/tweets?ids=123,456&tweet.fields=created_at,author_id&expansions=author_id&user.fields=username
7. Anti-Patterns¶
7.1 Chatty API¶
Problem: Requiring many round trips to accomplish a single task.
# Anti-pattern: Multiple calls needed
GET /api/users/123
GET /api/users/123/address
GET /api/users/123/preferences
GET /api/users/123/orders
GET /api/users/123/orders/latest
# Pattern: Composite endpoint or expansion
GET /api/users/123?expand=address,preferences,orders.latest
# Or: GraphQL
query {
user(id: "123") {
name
address { ... }
preferences { ... }
orders(first: 1) { ... }
}
}
Impact: - High latency (network round trips) - Increased load on servers - Poor mobile experience
7.2 Anemic API¶
Problem: API that exposes data but not behaviors.
# Anti-pattern: Client must know business logic
GET /api/orders/123
# Client calculates: total = sum(items.price * items.quantity) + tax + shipping
PATCH /api/orders/123
{
"status": "shipped"
}
# Client must know all valid status transitions
# Pattern: Expose behaviors as resources/actions
POST /api/orders/123:calculate-total
POST /api/orders/123:ship
POST /api/orders/123:refund
7.3 God Object API¶
Problem: Single endpoint that does too much.
# Anti-pattern
POST /api/process
{
"action": "create_user_and_send_email_and_subscribe_to_newsletter",
"user": {...},
"email_template": "welcome",
"newsletter_id": "weekly"
}
# Pattern: Separate concerns
POST /api/users
POST /api/users/123/welcome-email
POST /api/newsletters/weekly/subscribers
7.4 Leaky Abstraction¶
Problem: Exposing internal implementation details.
# Anti-pattern: Database schema leaked
{
"user_id": 123,
"user_tbl_created_dt": "2023-06-15",
"fk_address_id": 456,
"internal_status_code": 1
}
# Pattern: Clean domain model
{
"id": "usr_abc123",
"created_at": "2023-06-15T10:30:00Z",
"address": {
"id": "addr_def456"
},
"status": "active"
}
7.5 Breaking Changes Without Versioning¶
Problem: Making incompatible changes without warning.
Breaking Changes Include: - Removing fields - Renaming fields - Changing field types - Changing URL paths - Modifying required parameters - Changing error codes - Altering authentication
# Anti-pattern
# v1: GET /api/users/123 returns { "name": "John" }
# Later: GET /api/users/123 returns { "full_name": "John" }
# Pattern: Additive changes + deprecation period
{
"name": "John",
"full_name": "John Doe",
"_deprecations": [{
"field": "name",
"replacement": "full_name",
"sunset": "2024-01-01"
}]
}
7.6 Inconsistent Naming¶
Problem: Mixed naming conventions across the API.
// Anti-pattern: Mixed conventions
{
"user_id": "123",
"userName": "johndoe",
"EmailAddress": "john@example.com",
"created-at": "2023-06-15"
}
// Pattern: Consistent snake_case (or camelCase - pick one)
{
"user_id": "123",
"user_name": "johndoe",
"email_address": "john@example.com",
"created_at": "2023-06-15T10:30:00Z"
}
7.7 Missing Pagination¶
Problem: Returning unbounded result sets.
# Anti-pattern
GET /api/users
# Returns all 1 million users
# Pattern: Always paginate collections
GET /api/users?page_size=20&page_token=abc
{
"data": [...],
"pagination": {
"total_count": 1000000,
"page_size": 20,
"next_page_token": "def456"
}
}
7.8 Ignoring HTTP Semantics¶
Problem: Using wrong HTTP methods and status codes.
# Anti-pattern
POST /api/getUser?id=123
POST /api/deleteUser?id=123
GET /api/users (returns 200 with error in body)
# Pattern: Proper HTTP usage
GET /api/users/123
DELETE /api/users/123
GET /api/users
# Returns 404 for not found, 500 for errors
HTTP Method Semantics:
| Method | Idempotent | Safe | Cacheable | Body |
|---|---|---|---|---|
| GET | Yes | Yes | Yes | No |
| HEAD | Yes | Yes | Yes | No |
| POST | No | No | No* | Yes |
| PUT | Yes | No | No | Yes |
| PATCH | No | No | No | Yes |
| DELETE | Yes | No | No | Optional |
7.9 Over-Engineering¶
Problem: Adding unnecessary complexity.
# Anti-pattern: Over-engineered for simple CRUD
POST /api/v2/enterprises/ent_123/contexts/prod/resources/users/actions/create
{
"meta": {
"request_id": "...",
"correlation_id": "...",
"trace_parent": "..."
},
"data": {
"type": "user",
"attributes": {
"name": "John"
}
}
}
# Pattern: Simple and direct
POST /api/users
{
"name": "John"
}
7.10 N+1 Query Problem in API Design¶
Problem: API design that encourages N+1 data fetching.
# Anti-pattern: Forces N+1 queries
GET /api/orders
[
{"id": 1, "customer_id": 100},
{"id": 2, "customer_id": 101},
{"id": 3, "customer_id": 102}
]
# Client must make N additional calls for customer details
# Pattern 1: Support expansion
GET /api/orders?expand=customer
# Pattern 2: Support batch endpoints
POST /api/customers/batch
{
"ids": [100, 101, 102]
}
# Pattern 3: Use GraphQL data loader pattern
query {
orders {
id
customer { # Batched automatically
name
}
}
}
7.11 Security Anti-Patterns¶
Exposing Sensitive Data:
// Anti-pattern
{
"id": "usr_123",
"password_hash": "$2b$12$abc...",
"ssn": "123-45-6789",
"internal_flags": ["admin", "test_user"]
}
// Pattern: Filter sensitive fields
{
"id": "usr_123",
"email": "j***@example.com",
"ssn_last_four": "6789"
}
Sequential IDs:
# Anti-pattern: Predictable IDs enable enumeration
GET /api/users/1
GET /api/users/2
GET /api/users/3
# Pattern: Use UUIDs or prefixed random IDs
GET /api/users/usr_7Ks9xN2mP
7.12 Poor Error Handling¶
// Anti-pattern: Unhelpful errors
{
"error": true,
"message": "Error"
}
// Anti-pattern: Exposing stack traces
{
"error": "NullPointerException at com.example.UserService.getUser(UserService.java:123)"
}
// Pattern: Actionable, safe errors
{
"error": {
"code": "user_not_found",
"message": "User with ID 'usr_123' was not found",
"documentation_url": "https://docs.example.com/errors/user_not_found",
"request_id": "req_abc123"
}
}
8. Best Practices Checklist¶
Design Phase¶
- Use resource-oriented design (nouns, not verbs)
- Apply consistent naming conventions (snake_case or camelCase)
- Design for evolvability (versioning strategy)
- Document API with OpenAPI/AsyncAPI
- Define clear error codes and messages
Implementation Phase¶
- Use appropriate HTTP methods and status codes
- Implement pagination for all collections
- Support filtering, sorting, and field selection
- Add ETags and conditional request handling
- Implement idempotency for write operations
- Add rate limiting with proper headers
Security Phase¶
- Use HTTPS everywhere
- Implement proper authentication (OAuth 2.0, API keys)
- Validate all inputs
- Use non-sequential, random IDs
- Never expose sensitive data or internal errors
- Implement request signing for sensitive operations
Operations Phase¶
- Include request IDs in all responses
- Log requests with correlation IDs
- Monitor latency, error rates, throughput
- Set up alerts for anomalies
- Document deprecation timelines
- Provide migration guides for breaking changes
References¶
-
Zimmermann, O., Stocker, M., Lübke, D., Zdun, U., & Pautasso, C. (2022). Patterns for API Design: Simplifying Integration with Loosely Coupled Message Exchanges. Addison-Wesley.
-
Google Cloud API Design Guide: https://cloud.google.com/apis/design
-
Microsoft REST API Guidelines: https://github.com/microsoft/api-guidelines
-
Stripe API Reference: https://stripe.com/docs/api
-
AWS API Design: https://docs.aws.amazon.com/
-
JSON:API Specification: https://jsonapi.org/
-
RFC 7807 - Problem Details for HTTP APIs: https://tools.ietf.org/html/rfc7807
-
OpenAPI Specification: https://spec.openapis.org/oas/latest.html