返回 Skill 列表
extension
分类: 开发与工程无需 API Key

api-design

设计REST和GraphQL API。涵盖端点命名、版本控制、OpenAPI 3.1规范、分页、错误处理以及认证模式。

person作者: jakexiaohubgithub

API Design


Quick Start

Just describe your domain:

design an API for an e-commerce platform with products, orders, and customers

You'll get a complete endpoint design and OpenAPI snippet like:

paths:
  /v1/products:
    get:
      summary: List products
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200':
          description: Paginated product list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
  /v1/products/{id}:
    get:
      summary: Get product by ID
      responses:
        '200':
          description: Product detail
        '404':
          $ref: '#/components/responses/NotFound'

What to include in your request:

  • Domain or system name (e-commerce, billing, identity)
  • Key resources (users, products, orders)
  • Relationships (orders belong to users, orders have line items)
  • Consumer context (mobile app, internal service, public API)
  • Existing constraints (auth system, versioning requirements)

Triggers

| Trigger | Example | |---------|---------| | design an API for | "design an API for a task management system" | | create endpoints for | "create endpoints for user authentication" | | OpenAPI spec for | "OpenAPI spec for the billing service" | | GraphQL schema for | "GraphQL schema for a content management domain" | | review api | "review this API for problems" | | add endpoints for | "add endpoints for order fulfillment" | | design REST API | "design REST API for the notification service" |


Key Terms

| Term | Definition | |------|------------| | REST | Representational State Transfer — stateless, resource-oriented HTTP API style | | GraphQL | Query language for APIs with a single endpoint and client-driven data fetching | | OpenAPI | Standard specification format for describing REST APIs (current version: 3.1) | | Endpoint | A URL path + HTTP method combination that handles a specific operation | | Resource | A noun representing an entity in your domain (user, order, product) | | Schema | Formal definition of the shape and constraints of request/response data | | Idempotency | An operation that produces the same result no matter how many times it's called | | HATEOAS | Hypermedia As The Engine Of Application State — embedding navigable links in responses | | Pagination | Strategy for returning large datasets in chunks (offset-based or cursor-based) | | Rate Limiting | Restricting how often a client can call an endpoint within a time window |


Quick Reference

| Task | Approach | Key Consideration | |------|----------|-------------------| | New API | Start with resource identification | Model the domain, not the UI | | Versioning | URL path /v1/ for simplicity | Commit to support policy before launch | | Pagination | Cursor-based for large/live data | Offset breaks on concurrent writes | | Auth | Bearer token (JWT) as default | Scope to least privilege | | Error handling | { error: { type, message, subcode } } envelope | Machine-readable codes, human-readable messages | | GraphQL vs REST | REST for CRUD, GraphQL for flexible queries | Consider client diversity and caching needs |


Process Overview

Your API Requirements
    |
    v
+-----------------------------------------------------+
| Phase 1: REQUIREMENTS                               |
| * Identify resources and relationships              |
| * Determine consumers (mobile, web, service-to-service) |
| * Define auth model and access control needs        |
| * Enumerate constraints (rate limits, SLAs, quotas) |
+-----------------------------------------------------+
    |
    v
+-----------------------------------------------------+
| Phase 2: DESIGN                                     |
| * Define URL structure and resource hierarchy       |
| * Map HTTP methods to CRUD operations               |
| * Design request/response schemas                   |
| * Choose auth mechanism and scoping                 |
+-----------------------------------------------------+
    |
    v
+-----------------------------------------------------+
| Phase 3: SPECIFY                                    |
| * Generate OpenAPI 3.1 spec (or GraphQL SDL)        |
| * Define error codes and Problem Details format     |
| * Document pagination strategy and parameters       |
| * Add examples for every endpoint                   |
+-----------------------------------------------------+
    |
    v
+-----------------------------------------------------+
| Phase 4: VALIDATE                                   |
| * Run verification checklist                        |
| * Assess breaking change risk                       |
| * Review auth coverage                              |
| * Confirm documentation completeness               |
+-----------------------------------------------------+
    |
    v
Production-Ready API Specification

Commands

| Command | When to Use | Action | |---------|-------------|--------| | design api for {domain} | Starting fresh | Full endpoint design + OpenAPI spec | | add endpoints for {resource} | Extending existing API | New resource endpoints | | openapi spec for {api} | Formalizing existing design | Complete OpenAPI 3.1 document | | graphql schema for {domain} | GraphQL API | Type definitions + queries + mutations | | review api | Auditing existing API | Checklist audit + recommendations |

Workflow: Start with design api → formalize with openapi spec → extend with add endpoints → audit with review api


Core Principles

| Principle | WHY | Implementation | |-----------|-----|----------------| | Resource-oriented design | Resources are stable; operations on them are predictable | Nouns in URLs, verbs in HTTP methods | | Consistent naming | Reduces cognitive load for consumers | Plural nouns, kebab-case paths, camelCase fields | | Explicit versioning | Allows evolution without breaking clients | Version in URL path or header, committed from day one | | Defensive schemas | Fail fast on bad input; never trust caller data | Required fields, format constraints, max lengths |


Anti-Patterns

| Avoid | Why | Instead | |-------|-----|---------| | Verbs in URLs (/getUser, /createOrder) | Conflates HTTP method semantics | GET /users/{id}, POST /orders | | Inconsistent pluralization | Forces clients to memorize per-resource conventions | Always plural: /users, /orders, /products | | No versioning on day one | First breaking change forces emergency migration | Add /v1/ from the start | | Generic error responses ({"error": "failed"}) | Client can't distinguish or act on error | { error: { type, message, subcode } } with typed codes | | Breaking changes without deprecation | Breaks deployed clients silently | Additive changes only; Sunset header for removals | | Nesting resources more than 2 levels deep | URLs become unmaintainable and coupling increases | /orders/{id}/items yes; /users/{id}/orders/{id}/items/{id} no | | FLOAT for monetary values in responses | Floating-point rounding corrupts financial data | Return money as integer cents or string decimal | | Exposing internal IDs or database row numbers | Leaks implementation, complicates migration | Use opaque IDs (UUIDs or slugs) |


Verification Checklist

After designing an API:

  • [ ] All resource URLs use plural nouns
  • [ ] No verbs in URL paths
  • [ ] Nesting depth is 2 levels maximum
  • [ ] URL paths use kebab-case for multi-word segments
  • [ ] HTTP methods match semantics (GET=read, POST=create, PUT=replace, PATCH=partial, DELETE=remove)
  • [ ] PUT and DELETE are idempotent
  • [ ] Status codes are appropriate (201 for creation, 204 for no-content DELETE, 409 for conflict)
  • [ ] Version is in URL path or header, documented from day one
  • [ ] All endpoints with state mutation require auth
  • [ ] Error responses use { error: { type, message, subcode } } envelope
  • [ ] Validation errors include field-level detail
  • [ ] Pagination included for all collection endpoints
  • [ ] Pagination response includes meta.total, meta.cursor, or links.next
  • [ ] Money/currency fields use integer cents or string decimal (never float)
  • [ ] Dates use ISO 8601 in UTC (2025-10-31T14:30:00Z)
  • [ ] Content-Type is application/json on all JSON endpoints
  • [ ] Rate limiting headers documented (X-RateLimit-Limit, X-RateLimit-Remaining)
  • [ ] OpenAPI spec validates without errors
  • [ ] Every endpoint has at least one request/response example
  • [ ] Breaking changes assessed against existing consumers

See api-design-checklist.md for the full pre-launch review checklist. See openapi-templates.md for concrete OpenAPI 3.1 spec templates. See scalar-api-docs.md for interactive API documentation setup (Go, Python, TypeScript, Rust).


<details> <summary><strong>Deep Dive: REST Endpoint Design</strong></summary>

Resource Naming

| Rule | Good | Bad | |------|------|-----| | Plural nouns | /users, /orders | /user, /order | | kebab-case | /line-items, /tax-rates | /lineItems, /taxRates | | No verbs | POST /orders | POST /createOrder | | Opaque IDs | /users/usr_abc123 | /users/42 |

URL Structure

/v1/{resource}               # collection
/v1/{resource}/{id}          # single resource
/v1/{resource}/{id}/{child}  # sub-resource (max 2 levels)

Examples:

GET    /v1/orders              # list orders
POST   /v1/orders              # create order
GET    /v1/orders/{id}         # get order
PUT    /v1/orders/{id}         # replace order
PATCH  /v1/orders/{id}         # partial update
DELETE /v1/orders/{id}         # delete order
GET    /v1/orders/{id}/items   # list items for order
POST   /v1/orders/{id}/items   # add item to order

HTTP Method Mapping

| Method | Semantics | Idempotent | Body | |--------|-----------|------------|------| | GET | Read, no side effects | Yes | No | | POST | Create, trigger action | No | Yes | | PUT | Replace entire resource | Yes | Yes | | PATCH | Partial update | No* | Yes | | DELETE | Remove resource | Yes | No |

*PATCH can be made idempotent with conditional requests (If-Match)

Status Code Reference

| Code | Meaning | When to Use | |------|---------|-------------| | 200 | OK | Successful GET, PUT, PATCH | | 201 | Created | Successful POST creating a resource | | 204 | No Content | Successful DELETE, or PUT/PATCH with no body | | 400 | Bad Request | Malformed request, validation failure | | 401 | Unauthorized | Missing or invalid authentication | | 403 | Forbidden | Authenticated but not authorized | | 404 | Not Found | Resource does not exist | | 409 | Conflict | Duplicate create, optimistic lock failure | | 422 | Unprocessable Entity | Semantically invalid (business rule violation) | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Unexpected server failure | | 503 | Service Unavailable | Temporary outage, maintenance |

Sub-Resource Patterns

Keep sub-resources to actions that only make sense in context of the parent:

# Good: items only exist within an order
GET  /v1/orders/{id}/items
POST /v1/orders/{id}/items

# Prefer flat resources when entities stand alone
GET  /v1/items/{id}       # better than /v1/orders/{oid}/items/{iid}

For actions that don't map cleanly to CRUD, use a verb suffix as a last resort:

POST /v1/orders/{id}/cancel
POST /v1/orders/{id}/fulfill
POST /v1/users/{id}/verify-email
</details> <details> <summary><strong>Deep Dive: Response Format</strong></summary>

Envelope Structure

Use a consistent envelope for all responses:

{
  "data": { ... },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2025-10-31T14:30:00Z"
  },
  "links": {
    "self": "/v1/orders/ord_xyz",
    "next": "/v1/orders?cursor=eyJpZCI6MTAwfQ"
  },
  "errors": [ ... ]
}
  • data — the primary payload (object for single, array for collection)
  • meta — pagination cursors, request IDs, timestamps
  • links — HATEOAS navigation (self, next, prev, related actions)
  • errors — only present on error responses (4xx/5xx)

HATEOAS: Links and Actions

Use the links object for both navigation and available actions on the resource. Each key is a link relation type:

{
  "data": {
    "id": "ord_xyz",
    "status": "pending",
    "total": 1099
  },
  "links": {
    "self": { "href": "/v1/orders/ord_xyz" },
    "cancel": { "href": "/v1/orders/ord_xyz/cancel", "method": "POST" },
    "items": { "href": "/v1/orders/ord_xyz/items" },
    "payment": { "href": "/v1/orders/ord_xyz/pay", "method": "POST" }
  }
}

Key principles:

  • Actions are state-dependent — cancel only appears when the order is cancellable
  • Include method when the action is not a GET (avoids client guessing)
  • Use IANA-registered relation types where they exist (self, next, prev, collection)
  • Use descriptive custom relation types for domain actions (cancel, approve, payment)
  • Do NOT use a separate actions array — this is Siren format, not widely adopted. Standard links with method hints covers the same use case with less complexity

Field Naming Convention

| Convention | When | Example | |------------|------|---------| | camelCase | JSON APIs (default) | createdAt, lineItems | | snake_case | Python/Go backends matching language convention | created_at, line_items |

Pick one and apply it everywhere. Document it in your OpenAPI spec's x-naming-convention extension.

Null Handling

  • Omit optional fields that have no value rather than returning null where possible
  • When null is semantically meaningful (explicitly unset vs absent), include it with documentation
  • Never return null for arrays — return []

Date Format

Always ISO 8601 in UTC:

"createdAt": "2025-10-31T14:30:00Z"     # correct
"createdAt": "10/31/2025 2:30 PM"       # wrong — ambiguous and locale-dependent
"createdAt": 1730382600                 # avoid — requires client-side conversion

Monetary Values

{
  "amount": 1099,
  "currency": "USD",
  "amountFormatted": "$10.99"
}
  • Store and transmit as integer cents (or smallest currency unit)
  • Include currency code with every monetary field
  • Optionally include a pre-formatted display string for convenience
</details> <details> <summary><strong>Deep Dive: Versioning Strategies</strong></summary>

Strategy Comparison

| Strategy | Example | Pros | Cons | |----------|---------|------|------| | URL path | /v1/orders | Visible, cacheable, easy to test | Pollutes URL, not REST-pure | | Request header | Accept-Version: 2 | Clean URLs, REST-pure | Harder to test, requires header tooling | | Content negotiation | Accept: application/vnd.api.v2+json | Standards-compliant | Verbose, limited browser/tool support | | Query parameter | /orders?version=2 | Easy to test | Cache-unfriendly, easy to forget |

Recommendation

Default: URL path versioning.

  • Easiest to understand, document, and test
  • Works natively with all HTTP clients and proxies
  • Every environment can hit a specific version directly

Use header versioning only when you have multiple clients with very different needs and want a single URL namespace.

Version Lifecycle

v1 (stable)   ──────────────────────────────────► active
v2 (preview)  ────────► stable ─────────────────► active
v1 (sunset)             Sunset header added ──────► deprecated ──► removed

Timeline example:

  • v1 launch: /v1/ is stable, no deprecation
  • v2 launch: Announce v2, keep v1 running
  • v1 sunset: Add Sunset: Sat, 31 Dec 2026 23:59:59 GMT header to v1 responses
  • v1 removal: 6–12 months after Sunset header, remove v1

Backward-Compatible Changes (No Version Bump Needed)

  • Adding optional request fields
  • Adding new response fields
  • Adding new endpoints
  • Adding new enum values (if clients handle unknown values)
  • Relaxing validation (e.g., increasing max length)

Breaking Changes (Require Version Bump)

  • Removing or renaming fields
  • Changing field types
  • Removing endpoints
  • Changing HTTP methods
  • Tightening validation
  • Changing authentication requirements
</details> <details> <summary><strong>Deep Dive: Pagination</strong></summary>

Offset-Based vs Cursor-Based

| Factor | Offset | Cursor | |--------|--------|--------| | Implementation | Simple (LIMIT/OFFSET) | Moderate (encode last-seen ID) | | Performance on large datasets | Degrades (full table scan) | Consistent (index seek) | | Handles concurrent writes | Skips/duplicates items | Stable position | | Supports jump to page N | Yes | No | | Best for | Small datasets, admin UIs | Large/live datasets, feeds |

Offset Pagination Response

{
  "data": [ ... ],
  "meta": {
    "total": 1842,
    "page": 3,
    "perPage": 20,
    "totalPages": 93
  },
  "links": {
    "self": "/v1/orders?page=3&perPage=20",
    "prev": "/v1/orders?page=2&perPage=20",
    "next": "/v1/orders?page=4&perPage=20",
    "first": "/v1/orders?page=1&perPage=20",
    "last": "/v1/orders?page=93&perPage=20"
  }
}

Cursor Pagination Response

{
  "data": [ ... ],
  "meta": {
    "hasNext": true,
    "hasPrev": true,
    "cursor": {
      "next": "eyJpZCI6MTAwLCJjcmVhdGVkQXQiOiIyMDI1LTEwLTMxIn0=",
      "prev": "eyJpZCI6ODEsImNyZWF0ZWRBdCI6IjIwMjUtMTAtMzAifQ=="
    }
  }
}

Client usage:

GET /v1/orders?limit=20&after=eyJpZCI6MTAwfQ==

Cursor Implementation Pattern

Encode the sort key(s) and last-seen ID in the cursor:

{ "id": 100, "createdAt": "2025-10-31" }

Base64-encode this JSON — opaque to the client, but deterministic on the server.

Pagination Parameters

| Parameter | Offset | Cursor | |-----------|--------|--------| | page | Page number (1-indexed) | — | | perPage / limit | Items per page | Items per page | | after | — | Cursor for next page | | before | — | Cursor for prev page |

Always enforce a maximum limit (e.g., 100) server-side regardless of what the client requests.

</details> <details> <summary><strong>Deep Dive: Error Handling</strong></summary>

Error Response Format

Standard error envelope:

{
  "error": {
    "type": "BAD_REQUEST",
    "message": "Invalid email address format",
    "subcode": 12001
  }
}

| Field | Description | |-------|-------------| | error.type | Machine-readable error category (SCREAMING_SNAKE_CASE) | | error.message | Short, human-readable description of the error | | error.subcode | Numeric code for programmatic handling and documentation lookup |

Validation errors add a fields array:

{
  "error": {
    "type": "VALIDATION_FAILED",
    "message": "One or more fields failed validation",
    "subcode": 10001,
    "fields": [
      { "field": "quantity", "code": "INVALID_RANGE", "message": "Must be between 1 and 999" },
      { "field": "productId", "code": "NOT_FOUND", "message": "Product prod_zzz does not exist" }
    ]
  }
}

Request and Trace IDs

Every response (success and error) must include correlation headers:

X-Request-Id: req_abc123              # Server-generated if client omits
X-Trace-Id: trace_def456             # Distributed tracing correlation
Retry-After: 60                       # Seconds until retry (429, 503)
  • X-Request-Id: If the client sends one, echo it back. Otherwise, generate a UUID server-side. Log it with every request.
  • X-Trace-Id: Propagated through all downstream service calls for distributed tracing.
  • Do NOT put trace IDs in the error body. Headers are the canonical location — they're present on all responses (not just errors), are accessible to proxies/load balancers, and avoid coupling error schemas to tracing infrastructure. Clients needing to report an error to support should reference the X-Request-Id header value.

Error Code Registry

Define typed error codes as constants in your API:

VALIDATION_FAILED       422   One or more fields are invalid           10001
RESOURCE_NOT_FOUND      404   The requested resource does not exist     10002
DUPLICATE_RESOURCE      409   A resource with this identifier exists    10003
INSUFFICIENT_FUNDS      422   Account balance is insufficient           10004
RATE_LIMIT_EXCEEDED     429   Too many requests; retry after X seconds  10005
UNAUTHORIZED            401   Authentication credentials invalid        10006
FORBIDDEN               403   Caller lacks permission for this action   10007

Document the full registry in your OpenAPI spec under x-error-codes.

HTTP Status Decision Tree

Is the request malformed JSON?          → 400 Bad Request
Do the fields fail validation?          → 422 Unprocessable Entity
Is authentication missing or invalid?   → 401 Unauthorized
Is the user authenticated but denied?   → 403 Forbidden
Does the resource not exist?            → 404 Not Found
Does a conflict exist (duplicate)?      → 409 Conflict
Did a business rule fail?               → 422 Unprocessable Entity
Is the server broken?                   → 500 Internal Server Error
Is the service temporarily unavailable? → 503 Service Unavailable

RFC 9457 Compatibility

The error format above is intentionally simpler than RFC 9457 (Problem Details). If your API must comply with RFC 9457 (e.g., for interoperability with standards-mandating clients), use:

{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/v1/orders/ord_xyz"
}

Set Content-Type: application/problem+json for RFC 9457 responses.

</details> <details> <summary><strong>Deep Dive: Authentication and Authorization</strong></summary>

Auth Mechanism Comparison

| Mechanism | Best For | Complexity | Stateless | |-----------|----------|------------|-----------| | API Key | Server-to-server, simple integrations | Low | Yes | | Bearer JWT | User-facing APIs, short-lived tokens | Medium | Yes | | OAuth 2.0 + PKCE | User-delegated access, third-party integrations | High | Yes | | Session cookies | Traditional web apps, SSR | Low | No |

Bearer Token (JWT) Pattern

Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...

JWT payload convention:

{
  "sub": "usr_abc123",
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com",
  "exp": 1730386200,
  "iat": 1730382600,
  "scope": "orders:read orders:write"
}

API Key Pattern

X-API-Key: sk_live_abc123xyz
  • Prefix keys with environment (sk_live_, sk_test_) for instant recognition
  • Hash keys at rest; never store plaintext
  • Issue per-integration, not per-user
  • Support key rotation without downtime (accept both old and new during transition)

OAuth 2.0 Flows

| Flow | When | |------|------| | Authorization Code + PKCE | Web/mobile apps on behalf of a user | | Client Credentials | Service-to-service (no user context) | | Device Code | Devices without browsers (CLI tools, smart TVs) |

Scoping

Define scopes as {resource}:{action} pairs:

orders:read          Read orders
orders:write         Create and update orders
orders:delete        Delete orders
users:read           Read user profiles
admin:full           All access (restrict heavily)

Request minimum scopes. Validate scopes server-side on every request.

Rate Limiting Headers

Always return rate limit state in responses:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1730383200

On 429:

Retry-After: 47
</details> <details> <summary><strong>Deep Dive: OpenAPI 3.1 Specification</strong></summary>

Skeleton Template

openapi: 3.1.0
info:
  title: My Service API
  version: 1.0.0
  description: |
    Brief description of the API's purpose and audience.
  contact:
    name: Platform Team
    email: platform@example.com
  license:
    name: MIT

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://api-staging.example.com/v1
    description: Staging

security:
  - bearerAuth: []

paths:
  /resources:
    get:
      summary: List resources
      operationId: listResources
      tags: [Resources]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/After'
      responses:
        '200':
          description: Paginated resource list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    After:
      name: after
      in: query
      schema:
        type: string
        description: Cursor from previous response for next page

  responses:
    Unauthorized:
      description: Authentication credentials missing or invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'

  schemas:
    ProblemDetails:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        requestId:
          type: string

Reusable Schema Patterns

Envelope response:

ResourceListResponse:
  type: object
  properties:
    data:
      type: array
      items:
        $ref: '#/components/schemas/Resource'
    meta:
      $ref: '#/components/schemas/PaginationMeta'
    links:
      $ref: '#/components/schemas/PaginationLinks'

PaginationMeta:
  type: object
  properties:
    hasNext:
      type: boolean
    hasPrev:
      type: boolean

PaginationLinks:
  type: object
  properties:
    self:
      type: string
      format: uri
    next:
      type: string
      format: uri
    prev:
      type: string
      format: uri

$ref Strategy

  • All reusable schemas in components/schemas
  • All reusable parameters in components/parameters
  • All reusable responses in components/responses
  • Never inline schema objects in paths — always $ref

Validation Tooling

  • spectral — OpenAPI linting with configurable rulesets
  • redocly — Spec validation + doc generation
  • swagger-cli validate — Basic structural validation
</details> <details> <summary><strong>Deep Dive: GraphQL Schema Design</strong></summary>

Type Definitions

scalar DateTime
scalar Decimal

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

type Order {
  id: ID!
  status: OrderStatus!
  total: Decimal!
  currency: String!
  lineItems: [LineItem!]!
  createdAt: DateTime!
}

type LineItem {
  id: ID!
  product: Product!
  quantity: Int!
  unitPrice: Decimal!
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}

Query, Mutation, Subscription Patterns

type Query {
  # Single resource by ID
  order(id: ID!): Order

  # Paginated collection (Relay-style)
  orders(first: Int, after: String, filter: OrderFilter): OrderConnection!

  # Current user context
  me: User
}

type Mutation {
  # Returns the created/mutated resource
  createOrder(input: CreateOrderInput!): CreateOrderPayload!
  cancelOrder(id: ID!): CancelOrderPayload!
}

type Subscription {
  # Subscribe to events for a specific resource
  orderStatusChanged(orderId: ID!): Order!
}

Input Types

input CreateOrderInput {
  lineItems: [LineItemInput!]!
  shippingAddressId: ID!
  couponCode: String
}

input LineItemInput {
  productId: ID!
  quantity: Int!
}

Relay-Style Pagination (Connection Pattern)

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

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

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

Error Handling: Errors Array vs Union Types

Errors array (GraphQL default):

{
  "data": { "createOrder": null },
  "errors": [{ "message": "Product not found", "path": ["createOrder"] }]
}

Union result types (recommended for mutations):

union CreateOrderResult = Order | ValidationError | NotFoundError

type Mutation {
  createOrder(input: CreateOrderInput!): CreateOrderResult!
}

This approach makes errors first-class, typed, and queryable by clients.

Complexity and Depth Limiting

Prevent abuse by enforcing query complexity limits:

  • Max depth: 7–10 levels (reject deeply nested queries)
  • Max complexity: Assign cost to each field; reject queries over budget
  • Introspection: Disable in production for public APIs

Libraries: graphql-depth-limit, graphql-query-complexity

</details> <details> <summary><strong>Deep Dive: Breaking Change Management</strong></summary>

What Counts as Breaking

| Change | Breaking? | Notes | |--------|-----------|-------| | Add optional request field | No | Clients ignore unknown fields | | Add response field | No | Clients ignore unknown fields | | Remove request field | Yes | Existing clients may send it | | Remove response field | Yes | Existing clients may read it | | Rename field | Yes | Equivalent to remove + add | | Change field type | Yes | Parsing will fail | | Tighten validation | Yes | Previously valid requests rejected | | Loosen validation | No | More permissive | | Remove endpoint | Yes | 404 where clients expect 200 | | Change auth requirement | Yes | Auth failures for existing clients | | Add required request field | Yes | Existing requests missing the field | | Add new enum value | Usually no* | Depends on client handling unknown values |

*Clients should be designed to handle unknown enum values gracefully.

Additive-Only Evolution

Design for additive-only changes within a major version:

  • Add new optional fields to requests
  • Add new fields to responses
  • Add new endpoints
  • Add new optional query parameters
  • Add new enum values (with consumer guidance on handling unknowns)

Deprecation Process

Step 1: Mark deprecated in OpenAPI spec

/v1/orders/{id}/cancel:
  post:
    deprecated: true
    description: "Deprecated: Use PATCH /v1/orders/{id} with status=cancelled instead."

Step 2: Add Sunset header to responses

Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Deprecation: true
Link: <https://api.example.com/docs/migrations/v1-to-v2>; rel="deprecation"

Step 3: Notify consumers

  • Email registered API consumers
  • Post in developer changelog
  • Add in-response warning in meta.warnings array

Step 4: Enforce removal timeline

  • Minimum 6 months from Sunset header to removal for external APIs
  • Minimum 2 sprints for internal service-to-service APIs

Migration Guide Template

## Migrating from v1 to v2: Orders

### What changed
- `GET /v1/orders` response: `items` renamed to `lineItems`
- `POST /v1/orders` now requires `shippingAddressId`
- Error format changed from `{"error": "..."}` to `{ error: { type, message, subcode } }`

### Migration steps
1. Update `items``lineItems` in your order parsing code
2. Add `shippingAddressId` to order creation requests
3. Update error handling to read `type` and `detail` from responses

### Timeline
- v1 sunset: 2026-12-31
- v1 removal: 2027-03-31
</details>

Extension Points

  1. Framework-Specific Patterns: Express middleware, FastAPI dependency injection, Go net/http routing conventions
  2. API Gateway Integration: Kong, AWS API Gateway, Apigee — rate limiting, auth delegation, routing
  3. Contract Testing: Pact for consumer-driven contract tests; validate against OpenAPI spec with dredd
  4. SDK Generation: openapi-generator for client SDKs from OpenAPI spec; graphql-code-generator for TypeScript types
  5. Spec Validation Automation: spectral rulesets in CI, redocly lint for doc quality gates
  6. API Mocking: prism for mock servers from OpenAPI spec; enables parallel frontend/backend development
  7. Interactive Documentation: Scalar for modern, themeable API reference UI — see references/scalar-api-docs.md