|$ curl https://forge-ai.dev/api/markdown?path=docs/system-design/api-design
$cat docs/api-design.md
updated Recently·30 min read·published

API Design

System DesignIntermediate🎯Free Tools
Introduction

An API is a contract between systems. A well-designed API is predictable, discoverable, and resilient to change. In distributed systems, APIs are also the seams between services; their quality determines how easily the system can evolve.

This guide compares REST, GraphQL, and gRPC; covers versioning, pagination, idempotency, and rate limiting; and provides practical patterns for designing APIs that scale and fail gracefully.

Whether you are exposing a public API or defining internal service contracts, the principles here will help you build interfaces that other engineers want to use.

REST

REST (Representational State Transfer) models resources as URLs and uses HTTP methods to operate on them. It is stateless, cacheable, and leverages the semantics of HTTP status codes. REST is the default choice for public web APIs because it is simple, familiar, and works everywhere.

rest-design.txt
TEXT
1REST resource design:
2
3GET /users → list users
4GET /users/{id} → retrieve a user
5POST /users → create a user
6PUT /users/{id} → full update
7PATCH /users/{id} → partial update
8DELETE /users/{id} → delete a user
9
10GET /users/{id}/orders → list user's orders
11POST /users/{id}/orders → create an order for user
12
13Status codes:
14200 OK, 201 Created, 204 No Content
15400 Bad Request, 401 Unauthorized, 403 Forbidden
16404 Not Found, 409 Conflict, 429 Too Many Requests
17500 Internal Server Error, 502/503/504 Gateway errors
PrincipleMeaning
ResourcesURLs name things, not actions
StatelessnessEach request contains all context
Uniform interfaceStandard methods and media types
CacheabilityResponses declare cacheability

best practice

Use nouns for resources, plural by convention, and nest sub-resources no more than one level deep. Keep URLs stable; changing a URL is a breaking change for clients.
GraphQL

GraphQL is a query language for APIs. Clients request exactly the fields they need, and the server resolves the query by fetching data from underlying sources. GraphQL is powerful for mobile and composite UIs where over-fetching and under-fetching are common problems.

graphql.txt
TEXT
1GraphQL query:
2
3query {
4 user(id: "123") {
5 name
6 email
7 orders(first: 5) {
8 id
9 total
10 items {
11 name
12 price
13 }
14 }
15 }
16}
17
18GraphQL mutation:
19
20mutation {
21 createOrder(input: { userId: "123", items: [...] }) {
22 order {
23 id
24 total
25 }
26 errors
27 }
28}
AspectRESTGraphQL
Data shapeServer-defined endpointsClient-defined queries
Over-fetchingCommonAvoided
CachingHTTP caching works wellRequires custom strategies
ToolingOpenAPI, PostmanSchema introspection, Playground
Complexity riskEndpoint explosionQuery complexity attacks

warning

GraphQL shifts complexity to the server. You must implement query cost analysis, depth limiting, and complexity analysis to prevent expensive queries. Caching is harder because requests usually use POST and unique query strings.
gRPC

gRPC is a high-performance RPC framework that uses Protocol Buffers for serialization and HTTP/2 for transport. It supports unary calls, server streaming, client streaming, and bidirectional streaming. gRPC is ideal for internal service-to-service communication where both ends are controlled.

grpc.proto
TEXT
1Protocol Buffers service definition:
2
3service OrderService {
4 rpc GetOrder(GetOrderRequest) returns (Order);
5 rpc CreateOrder(CreateOrderRequest) returns (Order);
6 rpc StreamOrders(StreamOrdersRequest) returns (stream Order);
7}
8
9message GetOrderRequest {
10 string order_id = 1;
11}
12
13message Order {
14 string order_id = 1;
15 string user_id = 2;
16 repeated OrderItem items = 3;
17 double total = 4;
18}

info

Use gRPC for internal microservices. Use REST or GraphQL for client-facing and public APIs. Browser support for gRPC is limited without a proxy like gRPC-Web.
Versioning

APIs evolve. Versioning lets you introduce breaking changes without immediately breaking existing clients. The most common strategies are URL versioning, header versioning, and content negotiation.

StrategyExampleProsCons
URL path/v1/usersSimple, visibleClutters URLs
HeaderAPI-Version: v1Clean URLsLess discoverable
Content typeAccept: application/vnd.api.v1+jsonHTTP-nativeVerbose

best practice

Version at the resource or API level, not the individual field level. Maintain old versions only as long as necessary, and communicate deprecation timelines clearly. Avoid creating many minor versions.
Idempotency

An operation is idempotent if performing it multiple times has the same effect as performing it once. Idempotency is essential for distributed systems where network failures, retries, and duplicate requests are common. GET, PUT, and DELETE should be idempotent by design. POST for creation should use idempotency keys.

idempotency.txt
TEXT
1Idempotency key flow:
2
3Client ──POST /orders──▶ Server
4 Idempotency-Key: abc-123
5
6Server:
71. Check store for key abc-123
82. If found: return stored response
93. If not found: execute operation,
10 store response with key, return response
11
12Retry:
13Client ──POST /orders──▶ Server
14 Idempotency-Key: abc-123
15
16Server returns stored response; no duplicate created.

warning

Idempotency keys must be unique per operation and scoped to the user or account. They should expire after a reasonable window — typically 24 hours — to prevent unbounded storage growth.
Rate Limiting

Rate limiting protects APIs from abuse and ensures fair resource allocation. It restricts how many requests a client can make in a time window. Common algorithms include token bucket, leaky bucket, fixed window, and sliding window.

AlgorithmBehaviorBest For
Token bucketTokens added at fixed rate; each request consumes oneBursty traffic, general APIs
Leaky bucketRequests enter queue and exit at fixed rateSmoothing traffic
Fixed windowCount requests per time windowSimple implementations
Sliding windowCount requests over rolling intervalAvoids window-edge bursts

info

Return rate limit headers such as X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After. This lets clients adapt gracefully instead of blindly retrying into a 429 response.
Pagination

Pagination prevents APIs from returning unbounded result sets. The three common strategies are offset, cursor, and keyset pagination. Each has different performance and consistency tradeoffs.

StrategyHow It WorksProsCons
Offset?page=2&limit=20Simple, supports random accessSlow at deep pages, inconsistent under writes
Cursor?after=opaque_cursorConsistent, efficientCannot jump to arbitrary page
Keyset?last_id=123&limit=20Fast, stableTied to sort key

best practice

Prefer cursor or keyset pagination for high-volume lists. Offset pagination causes the database to scan and discard rows, becoming slower as the offset grows.
Error Handling and Resilience

APIs fail. How they fail determines whether clients can recover gracefully. Use standard HTTP status codes, include machine-readable error codes in the body, and avoid leaking internal details.

error-response.json
JSON
1{
2 "error": {
3 "code": "INSUFFICIENT_INVENTORY",
4 "message": "Requested quantity exceeds available stock.",
5 "target": "items[0].quantity",
6 "details": [
7 {
8 "code": "MAX_AVAILABLE",
9 "message": "Only 3 units are available.",
10 "target": "items[0].sku"
11 }
12 ]
13 }
14}

warning

Never return stack traces or internal identifiers to clients. Log full details server-side and return safe, actionable error messages to callers.
$Blueprint — Engineering Documentation·Section ID: SD-API-01·Revision: 1.0