REST API design patterns, structure, and best practices. Use when user asks to "design a REST API", "create API endpoints", "write OpenAPI spec", "design API routes", "add pagination to API", "version an API", "create API schema", "design webhook endpoints", "structure API responses", "implement HATEOAS", "design API errors", "API versioning", "API deprecation", "rate limiting design", or mentions REST API design, endpoint naming, HTTP methods, status codes, API best practices, request/response design, or API documentation.
Instrucciones de origen · Vista previa de solo lectura
name
api-design
description
REST API design patterns, structure, and best practices. Use when user asks to "design a REST API", "create API endpoints", "write OpenAPI spec", "design API routes", "add pagination to API", "version an API", "create API schema", "design webhook endpoints", "structure API responses", "implement HATEOAS", "design API errors", "API versioning", "API deprecation", "rate limiting design", or mentions REST API design, endpoint naming, HTTP methods, status codes, API best practices, request/response design, or API documentation.
A comprehensive skill for designing production-quality REST APIs. Covers resource naming, HTTP semantics, status codes, pagination, versioning, OpenAPI specs, authentication, error handling, and more.
Capabilities
Resource Naming - RESTful URI conventions and hierarchy design
HTTP Method Mapping - Correct use of GET, POST, PUT, PATCH, DELETE
Status Code Selection - Appropriate codes for every scenario
webhooks <events> - Design webhook endpoints for given events
version <strategy> - Apply versioning strategy to existing API
RESTful Resource Naming Conventions
Resources are nouns, not verbs. Use plural nouns for collections.
URI Structure
# Good - plural nouns, hierarchical
GET /users
GET /users/{userId}
GET /users/{userId}/orders
GET /users/{userId}/orders/{orderId}
POST /users/{userId}/orders
# Bad - verbs in path, singular nouns, flat structure
GET /getUser?id=123
POST /createOrder
GET /user/123/getOrders
DELETE /deleteUser/123
Naming Rules
Rule
Good
Bad
Plural nouns
/users
/user
Lowercase
/order-items
/OrderItems
Hyphens for readability
/order-items
/order_items or /orderItems
No verbs
/users/{id}/activate (POST)
/activateUser
No file extensions
/users
/users.json
Hierarchy via nesting
/users/{id}/posts
/user-posts?userId=1
Max 3 levels deep
/users/{id}/orders
/users/{id}/orders/{oid}/items/{iid}/reviews
Sub-Resources vs. Top-Level
# Sub-resource: order belongs to user (tight coupling)
GET /users/{userId}/orders/{orderId}
# Top-level: when resource is accessed independently
GET /orders/{orderId}
GET /orders?userId=123
# Use sub-resources when the child cannot exist without the parent
# Use top-level when the resource has its own identity
Actions on Resources
For non-CRUD operations, use a sub-resource verb as a last resort:
POST /users/{userId}/activate # state change
POST /orders/{orderId}/cancel # business action
POST /reports/generate # trigger process
POST /emails/{emailId}/resend # retry action
HTTP Methods
Method Semantics
Method
Purpose
Idempotent
Safe
Request Body
Response Body
GET
Read resource(s)
Yes
Yes
No
Yes
POST
Create resource / trigger action
No
No
Yes
Yes
PUT
Full replacement of resource
Yes
No
Yes
Yes (optional)
PATCH
Partial update of resource
No*
No
Yes
Yes
DELETE
Remove resource
Yes
No
No (usually)
No (usually)
HEAD
Same as GET, no body
Yes
Yes
No
No
OPTIONS
List allowed methods
Yes
Yes
No
Yes
*PATCH can be made idempotent with JSON Merge Patch (RFC 7396).
# Filtering - use field names as query params
GET /v1/users?status=active&role=admin&created_after=2025-01-01
# Sorting - prefix with - for descending
GET /v1/users?sort=-created_at,name
# Field selection - reduce payload size
GET /v1/users?fields=id,name,email
# Combined
GET /v1/users?status=active&sort=-created_at&fields=id,name&limit=10
Filtering Operators
For advanced filtering, use a structured syntax:
# LHS brackets style
GET /v1/products?price[gte]=10&price[lte]=100&name[contains]=widget
# Supported operators# eq - equals (default)# neq - not equals# gt - greater than# gte - greater than or equal# lt - less than# lte - less than or equal# in - in list: ?status[in]=active,pending# nin - not in list: ?status[nin]=deleted,archived# contains - substring: ?name[contains]=alice
API Versioning
Strategy 1: URL Path (Recommended)
GET /v1/users
GET /v2/users
Pros: Simple, visible, easy to route. Cons: Not purely RESTful.
Strategy 2: Custom Header
GET /users
Accept-Version: v2
# or
X-API-Version: 2
Pros: Clean URLs. Cons: Hidden, harder to test in browser.
Use application/problem+json content type for all errors.
Structure
{"type":"https://api.example.com/problems/validation-error","title":"Validation Error","status":422,"detail":"The request body contains invalid fields.","instance":"/v1/users","errors":[{"field":"email","message":"Must be a valid email address","code":"invalid_format"},{"field":"name","message":"Must be between 1 and 100 characters","code":"invalid_length"}]}
Common Error Types
// 400 Bad Request{"type":"https://api.example.com/problems/bad-request","title":"Bad Request","status":400,"detail":"The JSON body could not be parsed. Expected '}' at line 3, column 12."}// 401 Unauthorized{"type":"https://api.example.com/problems/unauthorized","title":"Unauthorized","status":401,"detail":"The access token has expired. Please refresh your token."}// 403 Forbidden{"type":"https://api.example.com/problems/forbidden","title":"Forbidden","status":403,"detail":"You do not have permission to delete users. Required scope: admin:users."}// 404 Not Found{"type":"https://api.example.com/problems/not-found","title":"Not Found","status":404,"detail":"No user found with ID 'usr_nonexistent'."}// 409 Conflict{"type":"https://api.example.com/problems/conflict","title":"Conflict","status":409,"detail":"A user with email 'alice@example.com' already exists."}
Pagination
Cursor-Based Pagination (Recommended)
Best for real-time data, large datasets, and when new records are frequently inserted.
# First page
GET /v1/users?limit=20
# Response includes cursor for next page
{
"data": [...],
"meta": {"has_more": true},
"links": {
"next": "/v1/users?cursor=eyJpZCI6InVzcl8wMjAifQ&limit=20"
}
}
# Next page
GET /v1/users?cursor=eyJpZCI6InVzcl8wMjAifQ&limit=20
Cursor implementation (base64-encoded JSON):
import base64, json
defencode_cursor(last_item):
payload = {"id": last_item["id"], "created_at": last_item["created_at"]}
return base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
defdecode_cursor(cursor):
return json.loads(base64.urlsafe_b64decode(cursor.encode()).decode())
# SQL query with cursor# SELECT * FROM users# WHERE (created_at, id) < (:cursor_created_at, :cursor_id)# ORDER BY created_at DESC, id DESC# LIMIT :limit + 1 -- fetch one extra to determine has_more
Offset-Based Pagination
Simpler but suffers from drift when data changes. Suitable for admin UIs and static data.
import hashlib, json
defhandle_request(request):
idempotency_key = request.headers.get("Idempotency-Key")
if idempotency_key:
# Check cache / database for previous result
cached = db.idempotency_cache.find_one({"key": idempotency_key})
if cached:
if cached["request_hash"] != hash_request(request):
return error(422, "Idempotency key reused with different parameters")
return cached["response"] # Return same response as before# Process the request
result = process_payment(request.json)
# Store result for future retriesif idempotency_key:
db.idempotency_cache.insert({
"key": idempotency_key,
"request_hash": hash_request(request),
"response": result,
"created_at": datetime.utcnow(),
"expires_at": datetime.utcnow() + timedelta(hours=24)
})
return result
defhash_request(request):
body = json.dumps(request.json, sort_keys=True)
return hashlib.sha256(body.encode()).hexdigest()
Guidelines
Require idempotency keys for all POST requests that create resources or trigger side effects
Keys should be client-generated UUIDs or prefixed random strings
Cache responses for 24 hours minimum
Return the same status code and body for replayed requests
Return 422 if the same key is reused with different request parameters
Allow consumers to list recent webhook events and retry delivery
Send a thin payload with resource ID; let consumer fetch full data if needed
Common Anti-Patterns to Avoid
1. Verbs in URLs
# Bad
POST /api/createUser
GET /api/getUsers
POST /api/deleteUser/123
# Good
POST /v1/users
GET /v1/users
DELETE /v1/users/123
2. Ignoring HTTP Methods
# Bad - using POST for everything
POST /api/users/get
POST /api/users/update
POST /api/users/delete
# Good - use proper HTTP methods
GET /v1/users
PUT /v1/users/{id}
DELETE /v1/users/{id}
3. Inconsistent Response Shapes
# Bad - different structures for different endpoints
GET /users -> [{"id": 1, "name": "Alice"}]
GET /users/1 -> {"id": 1, "name": "Alice", "email": "..."}
GET /orders -> {"orders": [...], "count": 10}
# Good - consistent envelope
GET /users -> {"data": [...], "meta": {...}}
GET /users/1 -> {"data": {...}}
GET /orders -> {"data": [...], "meta": {...}}
# Bad - sequential integers leak information
GET /v1/users/42
# Good - opaque identifiers
GET /v1/users/usr_a1b2c3d4
6. No Versioning
Always version your API from day one. Adding versioning later is a breaking change.
7. Deeply Nested Resources
# Bad - too many nesting levels
GET /v1/companies/123/departments/456/teams/789/members/012/tasks
# Good - flatten with query params
GET /v1/tasks?team_id=789&assignee_id=012
8. Missing Content Negotiation
Always set Content-Type on responses and respect Accept headers.
9. Returning Arrays as Root
# Bad - root array is vulnerable to JSON hijacking (legacy concern)
# and cannot be extended without breaking changes
[{"id": 1}, {"id": 2}]
# Good - object root allows adding metadata
{"data": [{"id": 1}, {"id": 2}], "meta": {"total": 2}}
10. No Rate Limiting
Every public API must have rate limits. Without them, a single client can degrade service for all users.
Quick Reference Checklist
When designing a new API, verify these items:
Resources use plural nouns (/users, not /user)
URLs are lowercase with hyphens (/order-items)
HTTP methods match semantics (GET reads, POST creates, etc.)
All responses use consistent envelope ({data, meta, links})
Errors use RFC 7807 Problem Details format
Pagination is implemented (cursor-based for public APIs)
Filtering, sorting, and field selection are supported
API is versioned from day one (/v1/...)
Authentication uses Bearer tokens or API keys in headers
Rate limiting headers are present on all responses
POST endpoints accept Idempotency-Key header
OpenAPI spec is complete and up to date
All dates use ISO 8601 format with timezone (2025-01-15T10:30:00Z)
Resource IDs are opaque strings with type prefixes (usr_, ord_)
201 responses include Location header
429 responses include Retry-After header
CORS headers are configured for browser clients
Request body validation returns 422 with field-level errors