AgentSkillsCN

api-design

在设计 API 时使用——无论是 REST 端点、GraphQL 模式、gRPC 服务,还是 WebSocket 协议——都需考虑资源命名、版本控制、分页、错误处理,以及 API 网关模式。 适用场景:REST API 设计、GraphQL 模式设计、gRPC 服务定义、WebSocket 协议设计、API 版本控制、分页策略、API 网关模式、幂等性、OpenAPI 规范。 切勿用于:数据存储设计(使用数据建模)、认证机制(使用认证)、API 测试(使用测试/API 测试)。

SKILL.md
--- frontmatter
name: api-design
description: |
    Use when designing APIs — REST endpoints, GraphQL schemas, gRPC services, or WebSocket protocols — including resource naming, versioning, pagination, error handling, and API gateway patterns.
    USE FOR: REST API design, GraphQL schema design, gRPC service definition, WebSocket protocol design, API versioning, pagination strategies, API gateway patterns, idempotency, OpenAPI specifications
    DO NOT USE FOR: data storage design (use data-modeling), authentication mechanisms (use authentication), API testing (use testing/api-testing)
license: MIT
metadata:
  displayName: "API Design Patterns"
  author: "Tyler-R-Kendrick"
compatibility: claude, copilot, cursor

API Design Patterns

Overview

API design determines how clients interact with backend services. A well-designed API is intuitive, consistent, evolvable, and resilient. This skill covers the four major API styles -- REST, GraphQL, gRPC, and WebSocket -- along with cross-cutting concerns like versioning, pagination, rate limiting, and idempotency.

REST API Design

Resource Naming Conventions

code
GET    /users              → List users
POST   /users              → Create a user
GET    /users/{id}         → Get a specific user
PUT    /users/{id}         → Replace a user
PATCH  /users/{id}         → Partially update a user
DELETE /users/{id}         → Delete a user

GET    /users/{id}/orders  → List orders for a user (sub-resource)

Rules:

  • Use nouns (not verbs) for resource names: /users not /getUsers.
  • Use plural nouns: /users not /user.
  • Use kebab-case for multi-word resources: /order-items not /orderItems.
  • Nest sub-resources only one level deep. Beyond that, promote to a top-level resource.

HTTP Methods & Status Codes

MethodSemanticsIdempotentSafe
GETRead a resourceYesYes
POSTCreate a resource / trigger actionNoNo
PUTReplace a resource entirelyYesNo
PATCHPartially update a resourceNo*No
DELETERemove a resourceYesNo

*PATCH can be made idempotent with careful design (e.g., JSON Merge Patch).

Status CodeWhen to Use
200 OKSuccessful GET, PUT, PATCH
201 CreatedSuccessful POST (include Location header)
204 No ContentSuccessful DELETE
400 Bad RequestMalformed request body or parameters
401 UnauthorizedMissing or invalid authentication
403 ForbiddenAuthenticated but insufficient permissions
404 Not FoundResource does not exist
409 ConflictState conflict (e.g., duplicate, version mismatch)
422 Unprocessable EntityValidation errors on well-formed request
429 Too Many RequestsRate limit exceeded (include Retry-After)
500 Internal Server ErrorUnhandled server error

HATEOAS (Hypermedia As The Engine Of Application State)

Include links in responses so clients can discover available actions:

json
{
  "id": "usr_42",
  "name": "Alice",
  "_links": {
    "self": { "href": "/users/usr_42" },
    "orders": { "href": "/users/usr_42/orders" },
    "deactivate": { "href": "/users/usr_42/deactivate", "method": "POST" }
  }
}

Pagination

StrategyProsCons
Offset-based (?offset=20&limit=10)Simple, supports jumping to page NInconsistent with concurrent writes; slow at large offsets
Cursor-based (?cursor=abc123&limit=10)Consistent during writes; performant at any depthCannot jump to arbitrary page; cursor is opaque

Recommendation: Use cursor-based pagination for any dataset that changes frequently or grows large. Use offset-based only for small, static datasets or when page-jumping is a hard requirement.

json
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTAwfQ==",
    "has_more": true
  }
}

Filtering & Sorting

code
GET /orders?status=shipped&created_after=2024-01-01&sort=-created_at&limit=20
  • Use query parameters for filtering. Prefix sort fields with - for descending.
  • For complex filtering, consider a structured query parameter: ?filter[status]=shipped&filter[total_gte]=100.

Versioning Strategies

StrategyExampleProsCons
URL path/v1/usersExplicit, easy to routeURL pollution, hard to sunset
HeaderAccept: application/vnd.api+json;version=2Clean URLsHidden, harder to test in browser
Content negotiationAccept: application/vnd.myapp.v2+jsonRESTful, media-type drivenComplex, less discoverable

Recommendation: URL-path versioning (/v1/, /v2/) is the most practical for most teams. Use it unless you have strong reasons for header-based versioning.

Richardson Maturity Model

LevelDescriptionExample
0 — The Swamp of POXSingle URI, single HTTP method (usually POST)POST /api with action in body
1 — ResourcesMultiple URIs, but only POST/GETGET /users, POST /users
2 — HTTP VerbsProper use of GET, POST, PUT, DELETE, status codesPUT /users/42 returns 200
3 — Hypermedia ControlsHATEOAS: responses include links to related actionsLinks in response body

Most production APIs target Level 2. Level 3 (HATEOAS) adds discoverability but increases response size and complexity.

GraphQL Schema Design

Schema Example (Schema-First / SDL)

graphql
type User {
  id: ID!
  name: String!
  email: String!
  orders(first: Int, after: String): OrderConnection!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  items: [OrderItem!]!
}

enum OrderStatus {
  PENDING
  SHIPPED
  DELIVERED
  CANCELLED
}

type OrderConnection {
  edges: [OrderEdge!]!
  pageInfo: PageInfo!
}

type OrderEdge {
  node: Order!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

type Query {
  user(id: ID!): User
  users(first: Int, after: String): UserConnection!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  cancelOrder(id: ID!): Order!
}

type Subscription {
  orderStatusChanged(userId: ID!): Order!
}

input CreateOrderInput {
  userId: ID!
  items: [OrderItemInput!]!
}

The N+1 Problem & DataLoader

code
Query: { users { orders { items } } }

Without DataLoader:
  1 query for users
  N queries for orders (one per user)     ← N+1 problem
  M queries for items (one per order)

With DataLoader:
  1 query for users
  1 batched query for all orders          ← solved
  1 batched query for all items

DataLoader batches and caches database lookups within a single request. It collects all keys requested during a single tick of the event loop, then issues a single batched query.

Schema-First vs. Code-First

ApproachToolsProsCons
Schema-firstApollo, graphql-toolsSchema is the contract; language-agnosticSchema and resolvers can drift
Code-firstNexus, TypeGraphQL, StrawberryType safety, co-located logicSchema is derived, less portable

Federation

For microservices, Apollo Federation (or similar) lets each service own part of the graph:

code
Service A owns: User { id, name, email }
Service B owns: User { orders: [Order] }   ← extends User
Gateway composes both into a single graph

gRPC Service Design

Protobuf Service Definition

protobuf
syntax = "proto3";

package orders.v1;

service OrderService {
  // Unary RPC
  rpc GetOrder(GetOrderRequest) returns (Order);

  // Server streaming
  rpc WatchOrderStatus(WatchOrderRequest) returns (stream OrderStatusEvent);

  // Client streaming
  rpc UploadOrderItems(stream OrderItem) returns (UploadSummary);

  // Bidirectional streaming
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message GetOrderRequest {
  string order_id = 1;
}

message Order {
  string id = 1;
  string user_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
  double total = 5;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_SHIPPED = 2;
  ORDER_STATUS_DELIVERED = 3;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double unit_price = 3;
}

Communication Patterns

PatternUse CaseFlow
UnaryStandard request-responseClient sends one message, server replies with one message
Server streamingLive updates, large result setsClient sends one message, server streams multiple responses
Client streamingFile upload, batch ingestionClient streams multiple messages, server replies once
Bidirectional streamingChat, real-time collaborationBoth sides stream messages independently

gRPC Best Practices

  • Deadlines: Always set deadlines on client calls. Propagate deadlines across service boundaries.
  • Interceptors: Use interceptors (middleware) for logging, authentication, and metrics.
  • Error codes: Use standard gRPC status codes (NOT_FOUND, INVALID_ARGUMENT, DEADLINE_EXCEEDED, etc.).
  • gRPC-Web: For browser clients, use Envoy or grpc-web proxy since browsers do not support HTTP/2 trailers natively.

WebSocket Protocol Design

Connection Lifecycle

code
1. Client sends HTTP Upgrade request
2. Server responds with 101 Switching Protocols
3. Full-duplex communication over persistent TCP connection
4. Either side can send frames at any time
5. Close handshake (close frame + acknowledgment)

Design Patterns

PatternDescription
Rooms / ChannelsGroup connections by topic; broadcast within a room (e.g., chat:room-42)
Heartbeat / Ping-PongPeriodic ping frames detect dead connections; server or client can initiate
Reconnection with backoffClient reconnects on disconnect with exponential backoff + jitter
Message acknowledgmentAssign IDs to messages; receiver acknowledges; sender retries unacknowledged

Message Format Convention

json
{
  "type": "order.status_changed",
  "payload": {
    "order_id": "ord_123",
    "new_status": "shipped"
  },
  "id": "msg_abc",
  "timestamp": "2024-01-15T14:30:00Z"
}

Cross-Cutting API Concerns

API Gateway Patterns

  • Request routing -- route by path, header, or method to the correct backend service.
  • Authentication offloading -- verify tokens at the gateway; pass claims to backends.
  • Rate limiting -- enforce quotas per client/API key at the gateway.
  • Response caching -- cache GET responses at the edge.
  • Request/response transformation -- reshape payloads between external and internal formats.

Idempotency Keys

For non-idempotent operations (especially payments), clients include a unique Idempotency-Key header. The server stores the result keyed by this value and returns the cached result on retry.

code
POST /payments
Idempotency-Key: pay_req_abc123
Content-Type: application/json

{ "amount": 99.99, "currency": "USD" }

OpenAPI / Swagger Documentation

For REST APIs, maintain an OpenAPI specification as the source of truth. Cross-reference specs for documentation standards. Generate client SDKs, server stubs, and interactive docs from the spec.

Best Practices

  • Design APIs for the consumer, not the database schema. Resource models should reflect use cases, not table structures.
  • Be consistent: once you pick conventions for naming, pagination, error format, and versioning, apply them uniformly across all endpoints.
  • Use pagination on every list endpoint from day one. Unpaginated lists become production incidents.
  • Prefer cursor-based pagination for any data that changes or grows.
  • Always set and propagate deadlines/timeouts. An API call without a timeout is a resource leak waiting to happen.
  • Include correlation IDs in every request/response for end-to-end tracing.
  • Document your API with OpenAPI (REST) or SDL (GraphQL) and keep the spec in version control alongside the code.