Skip to content

✍️ 필사 모드: API Design Complete Guide — REST, OpenAPI, Versioning, Pagination, Idempotency, Webhooks (2025)

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Prologue — "Great APIs are loved"

Open the Stripe API docs in April 2026. Requests, responses, and SDK examples live on a single page. Errors come with consistent codes and helpful messages. Versions are explicit (Stripe-Version: 2024-12-18). Rate limits, Idempotency, and Webhook signatures are standardized.

Why is Stripe called "the best API"? Consistency, predictability, documentation. That's design, not luck.

If performance is technical quality, API design is user experience — for your developer users. Good APIs are predictable, stable, forgiving, and transparent about limits and deprecation.


Part 1 — Richardson Maturity Model

Level 0: Single endpoint, POST everything
Level 1: Resource URLs (but verbs still leak in)
Level 2: Proper HTTP methods (GET/POST/PUT/DELETE)
Level 3: HATEOAS (hypermedia links in responses)

Reality: most APIs sit at Level 2. Level 3 HATEOAS is elegant in theory but rarely worth the overhead.


Part 2 — 10 REST Design Principles

1. Resource-oriented URLs

Good: /users/123/orders
Bad:  /getUserOrders?userId=123

2. Use plural nouns

Good: /users, /orders, /products
Bad:  /user, /order (mixed)

3. Respect HTTP method semantics

MethodMeaningIdempotent
GETRead (safe)Yes
POSTCreateNo
PUTFull replaceYes
PATCHPartial updateDepends
DELETEDeleteYes

4. Use status codes correctly

200 OK / 201 Created / 202 Accepted / 204 No Content
400 Bad Request / 401 Unauthorized / 403 Forbidden
404 Not Found / 409 Conflict / 422 Unprocessable / 429 Too Many Requests
500 / 502 / 503 / 504

Common mistake: always returning 200 with { "error": ... } in body. Never do this.

5. Consistent error format — RFC 9457

{
  "type": "https://example.com/errors/insufficient-funds",
  "title": "Insufficient funds",
  "status": 422,
  "detail": "Account balance $50 is less than required $100",
  "instance": "/accounts/12345/transactions/abc",
  "errors": [
    { "field": "amount", "message": "must be <= balance" }
  ],
  "requestId": "req_abc123"
}

RFC 9457 (Problem Details) replaced RFC 7807 in 2023.

6. Consistent response envelope

Pick one shape — envelope or bare object — and stick with it project-wide.

7. Times in ISO 8601 + UTC

{ "createdAt": "2026-04-15T12:34:56Z" }

8. Money as integer minor units

{ "amount": 10000, "currency": "KRW" }

Floats break precision.

9. Boolean names

Use isActive, hasPermission, canEdit — not ambiguous nouns.

10. IDs as strings with prefixes

{ "id": "usr_01HX9G7..." }

Prefixes (usr_, ord_) help debugging; sequential integers invite enumeration attacks.


Part 3 — OpenAPI 3.1 in Practice

Basic shape

openapi: 3.1.0
info:
  title: Example API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserList'
components:
  schemas:
    User:
      type: object
      required: [id, email]
      properties:
        id: { type: string, pattern: '^usr_' }
        email: { type: string, format: email }
        createdAt: { type: string, format: date-time }

Zod as single source of truth

import { z } from 'zod';
import { generateSchema } from '@anatine/zod-openapi';

export const User = z.object({
  id: z.string().startsWith('usr_'),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

export type User = z.infer<typeof User>;
const openApiSchema = generateSchema(User);

2025 tooling

ToolPurpose
Zod + @anatine/zod-openapiTS-first, auto schema
TypeboxType-friendly schema
Hono + zod-openapiRouter + OpenAPI auto
tRPC + trpc-openapitRPC to OpenAPI
FastAPI (Python)Pydantic + OpenAPI
SpectralLinting
Scalar, Mintlify, StoplightDocs UI

Spec-first vs Code-first: 2025 consensus is code-first. The spec is a derived artifact.


Part 4 — Versioning

Five approaches

  1. URL path/v1/users, /v2/users. Clear, cache-friendly.
  2. Accept headerAccept: application/vnd.example.v1+json. Clean URLs, harder to debug.
  3. Custom headerX-API-Version: 2024-12-18. Stripe's date-based approach.
  4. Query parameter?api-version=2. Not REST-y.
  5. Content negotiationAccept: application/json; version=2.

2025 recommendation

Public API:  URL path (v1, v2) + date header
Internal:    Header-based, rolling update
Mobile:      URL path, long-tail support

Stripe's date versioning

Every version is kept forever (customer opt-in); internally a version translator bridges them. The lesson: versioning is a long-term commitment.

Breaking vs non-breaking

Non-breaking: adding fields, endpoints, optional params, enum values (if clients handle unknowns).

Breaking: removing/renaming fields, changing types, adding required params, changing URLs or error codes.


Part 5 — Pagination

1. Offset-based

GET /users?limit=20&offset=40

Simple and UI-friendly, but deep offsets are slow and concurrent inserts cause duplicates/gaps.

2. Cursor-based

GET /users?limit=20&cursor=eyJpZCI6MTAwfQ==

{ "data": [...], "nextCursor": "eyJpZCI6MTIwfQ==", "hasMore": true }

Fast for deep pagination and stable order; no random access.

3. Keyset (seek method)

-- slow offset
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100000;
-- fast keyset
SELECT * FROM users WHERE id > 100020 ORDER BY id LIMIT 20;

Very fast, index-driven; changing the sort column gets complicated.

Choose

Paged UI ("1 2 3 ... 10")Offset
Infinite scroll / APICursor
Internal batch, huge tables → Keyset

Part 6 — Idempotency

User taps "Pay", network times out, client retries. The server sees two requests, charges twice. The fix is an Idempotency Key.

Stripe pattern

POST /charges HTTP/1.1
Idempotency-Key: f8a9b2c3-...-xyz
Content-Type: application/json

{ "amount": 10000, "currency": "KRW" }

Server logic:

1. Check Idempotency-Key
2. If present:
   - same body    -> return stored response
   - different    -> 409 Conflict
3. If absent: process and cache result (24h TTL)

When required

  • GET/PUT/DELETE — naturally idempotent
  • POST — must accept Idempotency-Key
  • PATCH — design dependent

Node + Redis sketch

async function idempotentHandler(req, res) {
  const key = req.headers['idempotency-key'];
  if (!key) return res.status(400).json({ error: 'Idempotency-Key required' });

  const lock = await redis.set(`lock:${key}`, '1', 'NX', 'EX', 30);
  if (!lock) await sleep(500);

  const cached = await redis.get(`idempotency:${key}`);
  if (cached) {
    const { bodyHash, response } = JSON.parse(cached);
    if (bodyHash !== hash(req.body)) {
      return res.status(409).json({ error: 'Conflicting request' });
    }
    return res.status(response.status).json(response.body);
  }

  const result = await processPayment(req.body);
  await redis.set(
    `idempotency:${key}`,
    JSON.stringify({ bodyHash: hash(req.body), response: { status: 200, body: result } }),
    'EX', 86400
  );
  return res.json(result);
}

Part 7 — Rate Limiting

Three algorithms

  • Token Bucket — tokens refill at a steady rate; bursts allowed up to bucket size. Easy.
  • Leaky Bucket — queue drains at a fixed rate; bursts smoothed. Medium.
  • Sliding Window — precise count over last N seconds via Redis sorted sets. Hard but accurate.

Response headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1697050800

# Or RFC 9466 (draft):
RateLimit-Limit: 100, 100;w=60
RateLimit-Remaining: 42
RateLimit-Reset: 58

429 with Retry-After

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{ "error": "Rate limit exceeded", "retryAfter": 60 }

Implement via Redis + Lua, Cloudflare, Envoy Rate Limit, or API gateways (Kong, Tyk, Traefik).


Part 8 — Webhook Design

Seven essentials

  1. Signing — HMAC-SHA256 over timestamp + "." + body, sent as X-Example-Signature: t=...,v1=.... Verifies origin and prevents tampering.
function verifyWebhook(body: string, signature: string, secret: string): boolean {
  const [t, v1] = signature.split(',').map(s => s.split('=')[1]);
  const timestamp = parseInt(t);
  if (Math.abs(Date.now()/1000 - timestamp) > 300) return false;
  const expected = hmacSha256(secret, `${t}.${body}`);
  return timingSafeEqual(expected, v1);
}
  1. Timestamp — reject messages older than 5 minutes to block replay.
  2. Retry with backoff — e.g., 1m, 5m, 15m, 1h, 6h, 24h for up to 3 days.
  3. Idempotency — include event ID so receivers dedupe.
  4. Ordered vs unordered — Stripe uses unordered; receivers re-fetch latest state.
  5. VersionAccept-Version header or apiVersion in payload.
  6. Testing/replay — dashboard re-send, Stripe CLI stripe listen, never disable signature checks in dev.

Receiver checklist

1. Verify signature first
2. Check timestamp (<= 5 minutes)
3. Dedupe by event ID
4. Return 200 immediately; queue heavy work
5. Async processing (Bull, Celery, etc.)
6. Return 5xx on failure so sender retries

Part 9 — AsyncAPI and Events

CloudEvents (CNCF standard)

{
  "specversion": "1.0",
  "type": "com.example.order.created",
  "source": "/orders",
  "id": "evt_abc",
  "time": "2026-04-15T12:00:00Z",
  "datacontenttype": "application/json",
  "data": { "orderId": 123, "amount": 10000 }
}

AsyncAPI 3.0

asyncapi: 3.0.0
info:
  title: Order Service Events
  version: 1.0.0
channels:
  orderCreated:
    address: orders.created
    messages:
      orderCreatedMessage:
        payload:
          type: object
          properties:
            orderId: { type: string }
            amount: { type: integer }

Webhook vs Queue vs Stream

PatternShapeExamples
WebhookHTTP pushStripe, GitHub
QueuePull, 1:1SQS, RabbitMQ
StreamPull, 1:N, orderedKafka, Kinesis

External integration → Webhook. Internal jobs → Queue. Audit/analytics → Stream.


Part 10 — REST vs GraphQL vs gRPC vs tRPC

REST + OpenAPI: public APIs, caching, diverse clients
GraphQL:        complex graphs, client-driven field selection
gRPC:           internal microservices, perf, polyglot
tRPC:           TS monorepo full-stack, fast dev

TypeSpec (Microsoft) is rising — a concise IDL that compiles to OpenAPI.


Part 11 — Security and Auth

MethodNotes
API KeySimple, rotation hard
OAuth 2.0User delegation, complex
JWTStateless, revocation hard
Session CookieEasy revocation, mind CSRF
mTLSService-to-service, strong

API key best practices

  • Prefix (sk_live_, pk_test_) for type clarity
  • At least 32 random characters
  • Store hashed; compare hash to hash
  • Scope permissions (read-only, admin)
  • Rotate cleanly (old+new overlap)
  • Enroll in secret scanners (GitHub)

CORS

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key
Access-Control-Max-Age: 86400

Browsers reject Allow-Origin: * combined with credentials — always name the origin.


Part 12 — Deprecation and Sunset

Sunset header (RFC 8594)

Sunset: Wed, 01 Jan 2027 00:00:00 GMT
Deprecation: true
Link: <https://docs.example.com/v2>; rel="successor-version"

Policy: 6 months ahead start deprecation header, 3 months email, 1 month final notice, after sunset return 410 Gone. Provide migration guides, usage dashboards, and SDK warnings.


Part 13 — 6-Month Roadmap

  • Month 1: REST basics, methods, status codes, errors
  • Month 2: OpenAPI 3.1 with Zod + zod-openapi
  • Month 3: Pagination (all three styles), cursor encoding
  • Month 4: Idempotency + Rate Limiting on Redis
  • Month 5: Webhook system — signing, retries, dashboard
  • Month 6: Docs automation (Scalar/Mintlify), SDK generation

Part 14 — 12-Item Checklist

  • OpenAPI 3.1 auto-generated (code-first)
  • HTTP methods / status codes consistent
  • Errors follow RFC 9457
  • Idempotency-Key required on POST
  • Rate-limit headers
  • Cursor pagination for new endpoints
  • Webhook signing (HMAC-SHA256)
  • Webhook retries + dashboard
  • Versioning policy documented
  • Sunset/Deprecation headers
  • Explicit CORS origins
  • Auto-generated SDKs

Part 15 — 10 Anti-Patterns

  1. 200 for every error
  2. Verbs in URLs (/getUserById)
  3. Lists without pagination
  4. Payment APIs without idempotency
  5. Inconsistent error shapes
  6. Breaking changes without new version
  7. Skipping webhook signature verification
  8. No rate limiting on public endpoints
  9. Exposing sequential integer IDs
  10. Deleting endpoints without deprecation notice

Closing — "An API is a contract"

Once published, an API is hard to change. Customers depend on it. Breaking changes cost real money. Design carefully, document thoroughly, support for the long haul.

The 2025 ten commandments:

  1. Resource-oriented, plural URLs
  2. Correct HTTP methods and status codes
  3. RFC 9457 errors
  4. OpenAPI 3.1 as single source of truth
  5. Clear versioning policy
  6. Cursor pagination
  7. Idempotency-Key on POST
  8. Rate limits with standard headers
  9. Webhook signing + retries + dashboard
  10. Deprecation and sunset headers

Next up — SaaS Architecture Complete Guide: multi-tenancy, billing, feature flags, audit logs, RBAC/ABAC, usage metering. You built the API. Now sell it.

현재 단락 (1/284)

Open the Stripe API docs in April 2026. Requests, responses, and SDK examples live on a single page....

작성 글자: 0원문 글자: 11,505작성 단락: 0/284