api-design
affaan-m/ecc
REST API design patterns for resource naming, status codes, pagination, filtering, versioning, and rate limiting.
What is api-design?
Conventions and best practices for designing consistent, developer-friendly REST APIs. Use this when designing new endpoints, reviewing API contracts, implementing error handling, or planning versioning and rate-limiting strategies for production APIs.
- Define resource naming conventions (kebab-case, plural nouns, sub-resources for relationships)
- Choose appropriate HTTP methods and status codes (200, 201, 204, 400, 401, 403, 404, 422, 429, 500, etc.)
- Implement pagination strategies (offset-based for small datasets, cursor-based for scalability)
- Design filtering, sorting, and search query parameters with standard notation
- Structure consistent success and error response envelopes with metadata and links
- Plan API versioning and authentication patterns (Bearer tokens, API keys, role-based authorization)
How to install api-design
npx skills add null --skill api-designHow to use api-design
- 1.Review the resource design section to establish URL structure and naming conventions for your endpoints
- 2.Select appropriate HTTP methods and status codes for each operation (GET, POST, PUT, PATCH, DELETE)
- 3.Choose a pagination strategy: offset-based for small datasets or cursor-based for large/infinite-scroll use cases
- 4.Define filtering, sorting, and search parameters using the recommended query parameter notation
- 5.Structure response envelopes with data, meta, and links fields for consistency
- 6.Implement error responses with field-level details for validation failures
- 7.Add rate-limiting headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) and 429 responses
Use cases
- Designing new REST API endpoints for a web service or microservice
- Reviewing and standardizing existing API contracts across a team
- Adding pagination and filtering to collection endpoints handling large datasets
- Implementing field-level error responses and validation error handling
- Planning API versioning strategy for backward compatibility
- Backend engineers designing or reviewing APIs
- Full-stack developers building API-first applications
- API architects planning production API strategies
- Teams standardizing API conventions across services
api-design FAQ
Use offset-based pagination for small datasets (<10K) and admin dashboards where users expect page numbers. Use cursor-based pagination for infinite scroll, feeds, large datasets, and public APIs where performance is critical.
Use 400 Bad Request for malformed JSON or invalid request format, and 422 Unprocessable Entity for semantically invalid data (valid JSON but business logic violations). Include field-level error details in the response body.
Use a consistent error envelope with code, message, and optional details array. Include field-specific information for validation errors with field name, message, and error code. Never expose internal server details in 500 errors.
Use Bearer tokens in the Authorization header for user-facing APIs. Use X-API-Key header for server-to-server communication. Always validate tokens and check resource-level authorization (ownership) in addition to authentication.
Include version in the URL path (e.g., /api/v1/users). This makes versioning explicit and allows clients to upgrade at their own pace. Plan for backward compatibility and deprecation timelines.
Full instructions (SKILL.md)
Source of truth, from affaan-m/ecc.
name: api-design description: REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs. metadata: origin: ECC
API Design Patterns
Conventions and best practices for designing consistent, developer-friendly REST APIs.
When to Activate
- Designing new API endpoints
- Reviewing existing API contracts
- Adding pagination, filtering, or sorting
- Implementing error handling for APIs
- Planning API versioning strategy
- Building public or partner-facing APIs
Resource Design
URL Structure
# Resources are nouns, plural, lowercase, kebab-case
GET /api/v1/users
GET /api/v1/users/:id
POST /api/v1/users
PUT /api/v1/users/:id
PATCH /api/v1/users/:id
DELETE /api/v1/users/:id
# Sub-resources for relationships
GET /api/v1/users/:id/orders
POST /api/v1/users/:id/orders
# Actions that don't map to CRUD (use verbs sparingly)
POST /api/v1/orders/:id/cancel
POST /api/v1/auth/login
POST /api/v1/auth/refresh
Naming Rules
# GOOD
/api/v1/team-members # kebab-case for multi-word resources
/api/v1/orders?status=active # query params for filtering
/api/v1/users/123/orders # nested resources for ownership
# BAD
/api/v1/getUsers # verb in URL
/api/v1/user # singular (use plural)
/api/v1/team_members # snake_case in URLs
/api/v1/users/123/getOrders # verb in nested resource
HTTP Methods and Status Codes
Method Semantics
| Method | Idempotent | Safe | Use For |
|---|---|---|---|
| GET | Yes | Yes | Retrieve resources |
| POST | No | No | Create resources, trigger actions |
| PUT | Yes | No | Full replacement of a resource |
| PATCH | No* | No | Partial update of a resource |
| DELETE | Yes | No | Remove a resource |
*PATCH can be made idempotent with proper implementation
Status Code Reference
# Success
200 OK — GET, PUT, PATCH (with response body)
201 Created — POST (include Location header)
204 No Content — DELETE, PUT (no response body)
# Client Errors
400 Bad Request — Validation failure, malformed JSON
401 Unauthorized — Missing or invalid authentication
403 Forbidden — Authenticated but not authorized
404 Not Found — Resource doesn't exist
409 Conflict — Duplicate entry, state conflict
422 Unprocessable Entity — Semantically invalid (valid JSON, bad data)
429 Too Many Requests — Rate limit exceeded
# Server Errors
500 Internal Server Error — Unexpected failure (never expose details)
502 Bad Gateway — Upstream service failed
503 Service Unavailable — Temporary overload, include Retry-After
Common Mistakes
# BAD: 200 for everything
{ "status": 200, "success": false, "error": "Not found" }
# GOOD: Use HTTP status codes semantically
HTTP/1.1 404 Not Found
{ "error": { "code": "not_found", "message": "User not found" } }
# BAD: 500 for validation errors
# GOOD: 400 or 422 with field-level details
# BAD: 200 for created resources
# GOOD: 201 with Location header
HTTP/1.1 201 Created
Location: /api/v1/users/abc-123
Response Format
Success Response
{
"data": {
"id": "abc-123",
"email": "alice@example.com",
"name": "Alice",
"created_at": "2025-01-15T10:30:00Z"
}
}
Collection Response (with Pagination)
{
"data": [
{ "id": "abc-123", "name": "Alice" },
{ "id": "def-456", "name": "Bob" }
],
"meta": {
"total": 142,
"page": 1,
"per_page": 20,
"total_pages": 8
},
"links": {
"self": "/api/v1/users?page=1&per_page=20",
"next": "/api/v1/users?page=2&per_page=20",
"last": "/api/v1/users?page=8&per_page=20"
}
}
Error Response
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"code": "invalid_format"
},
{
"field": "age",
"message": "Must be between 0 and 150",
"code": "out_of_range"
}
]
}
}
Response Envelope Variants
// Option A: Envelope with data wrapper (recommended for public APIs)
interface ApiResponse<T> {
data: T;
meta?: PaginationMeta;
links?: PaginationLinks;
}
interface ApiError {
error: {
code: string;
message: string;
details?: FieldError[];
};
}
// Option B: Flat response (simpler, common for internal APIs)
// Success: just return the resource directly
// Error: return error object
// Distinguish by HTTP status code
Pagination
Offset-Based (Simple)
GET /api/v1/users?page=2&per_page=20
# Implementation
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;
Pros: Easy to implement, supports "jump to page N" Cons: Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts
Cursor-Based (Scalable)
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
# Implementation
SELECT * FROM users
WHERE id > :cursor_id
ORDER BY id ASC
LIMIT 21; -- fetch one extra to determine has_next
{
"data": [...],
"meta": {
"has_next": true,
"next_cursor": "eyJpZCI6MTQzfQ"
}
}
Pros: Consistent performance regardless of position, stable with concurrent inserts Cons: Cannot jump to arbitrary page, cursor is opaque
When to Use Which
| Use Case | Pagination Type |
|---|---|
| Admin dashboards, small datasets (<10K) | Offset |
| Infinite scroll, feeds, large datasets | Cursor |
| Public APIs | Cursor (default) with offset (optional) |
| Search results | Offset (users expect page numbers) |
Filtering, Sorting, and Search
Filtering
# Simple equality
GET /api/v1/orders?status=active&customer_id=abc-123
# Comparison operators (use bracket notation)
GET /api/v1/products?price[gte]=10&price[lte]=100
GET /api/v1/orders?created_at[after]=2025-01-01
# Multiple values (comma-separated)
GET /api/v1/products?category=electronics,clothing
# Nested fields (dot notation)
GET /api/v1/orders?customer.country=US
Sorting
# Single field (prefix - for descending)
GET /api/v1/products?sort=-created_at
# Multiple fields (comma-separated)
GET /api/v1/products?sort=-featured,price,-created_at
Full-Text Search
# Search query parameter
GET /api/v1/products?q=wireless+headphones
# Field-specific search
GET /api/v1/users?email=alice
Sparse Fieldsets
# Return only specified fields (reduces payload)
GET /api/v1/users?fields=id,name,email
GET /api/v1/orders?fields=id,total,status&include=customer.name
Authentication and Authorization
Token-Based Auth
# Bearer token in Authorization header
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# API key (for server-to-server)
GET /api/v1/data
X-API-Key: sk_live_abc123
Authorization Patterns
// Resource-level: check ownership
app.get("/api/v1/orders/:id", async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: { code: "not_found" } });
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
return res.json({ data: order });
});
// Role-based: check permissions
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
await User.delete(req.params.id);
return res.status(204).send();
});
Rate Limiting
Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
# When exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 60 seconds."
}
}
Rate Limit Tiers
| Tier | Limit | Window | Use Case |
|---|---|---|---|
| Anonymous | 30/min | Per IP | Public endpoints |
| Authenticated | 100/min | Per user | Standard API access |
| Premium | 1000/min | Per API key | Paid API plans |
| Internal | 10000/min | Per service | Service-to-service |
Versioning
URL Path Versioning (Recommended)
/api/v1/users
/api/v2/users
Pros: Explicit, easy to route, cacheable Cons: URL changes between versions
Header Versioning
GET /api/users
Accept: application/vnd.myapp.v2+json
Pros: Clean URLs Cons: Harder to test, easy to forget
Versioning Strategy
1. Start with /api/v1/ — don't version until you need to
2. Maintain at most 2 active versions (current + previous)
3. Deprecation timeline:
- Announce deprecation (6 months notice for public APIs)
- Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
- Return 410 Gone after sunset date
4. Non-breaking changes don't need a new version:
- Adding new fields to responses
- Adding new optional query parameters
- Adding new endpoints
5. Breaking changes require a new version:
- Removing or renaming fields
- Changing field types
- Changing URL structure
- Changing authentication method
Implementation Patterns
TypeScript (Next.js API Route)
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const parsed = createUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({
error: {
code: "validation_error",
message: "Request validation failed",
details: parsed.error.issues.map(i => ({
field: i.path.join("."),
message: i.message,
code: i.code,
})),
},
}, { status: 422 });
}
const user = await createUser(parsed.data);
return NextResponse.json(
{ data: user },
{
status: 201,
headers: { Location: `/api/v1/users/${user.id}` },
},
);
}
Python (Django REST Framework)
from rest_framework import serializers, viewsets, status
from rest_framework.response import Response
class CreateUserSerializer(serializers.Serializer):
email = serializers.EmailField()
name = serializers.CharField(max_length=100)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "email", "name", "created_at"]
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
if self.action == "create":
return CreateUserSerializer
return UserSerializer
def create(self, request):
serializer = CreateUserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = UserService.create(**serializer.validated_data)
return Response(
{"data": UserSerializer(user).data},
status=status.HTTP_201_CREATED,
headers={"Location": f"/api/v1/users/{user.id}"},
)
Go (net/http)
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
if err := req.Validate(); err != nil {
writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
return
}
user, err := h.service.Create(r.Context(), req)
if err != nil {
switch {
case errors.Is(err, domain.ErrEmailTaken):
writeError(w, http.StatusConflict, "email_taken", "Email already registered")
default:
writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
}
return
}
w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
writeJSON(w, http.StatusCreated, map[string]any{"data": user})
}
API Design Checklist
Before shipping a new endpoint:
- Resource URL follows naming conventions (plural, kebab-case, no verbs)
- Correct HTTP method used (GET for reads, POST for creates, etc.)
- Appropriate status codes returned (not 200 for everything)
- Input validated with schema (Zod, Pydantic, Bean Validation)
- Error responses follow standard format with codes and messages
- Pagination implemented for list endpoints (cursor or offset)
- Authentication required (or explicitly marked as public)
- Authorization checked (user can only access their own resources)
- Rate limiting configured
- Response does not leak internal details (stack traces, SQL errors)
- Consistent naming with existing endpoints (camelCase vs snake_case)
- Documented (OpenAPI/Swagger spec updated)
Related skills
More from affaan-m/ecc and the wider catalog.
architecture-decision-records
Capture architectural decisions as structured ADRs during coding sessions so future developers understand the reasoning behind your codebase.
article-writing
Write long-form content in a distinctive voice, from blog posts to guides, without sounding like an LLM.
automation-audit-ops
Evidence-first inventory and overlap audit for automation jobs, hooks, connectors, and MCP servers.
autonomous-loops
Patterns and architectures for autonomous Claude Code loops, from sequential pipelines to RFC-driven multi-agent DAG systems.
backend-patterns
Backend architecture patterns, API design, database optimization, and best practices for Node.js, Express, and Next.js.
benchmark
Measure performance baselines, detect regressions, and compare stack alternatives before/after changes.