travisjneuman avatar

api-design

REST and GraphQL API design best practices including OpenAPI specs. Use when designing APIs, documen

提供方 travisjneuman|开源

API Design Guide

Best practices for designing developer-friendly, maintainable APIs.

REST API Principles

Resource Naming

Use nouns, not verbs:

✓ GET /users
✓ GET /users/123
✓ GET /users/123/orders

✗ GET /getUsers
✗ GET /fetchUserById
✗ POST /createNewOrder

Use plural nouns:

✓ /users
✓ /orders
✓ /products

✗ /user
✗ /order

Hierarchical relationships:

/users/{userId}/orders           # User's orders
/users/{userId}/orders/{orderId} # Specific order

HTTP Methods

MethodPurposeIdempotentRequest Body
GETRetrieve resource(s)YesNo
POSTCreate resourceNoYes
PUTReplace resource entirelyYesYes
PATCHUpdate resource partiallyNoYes
DELETERemove resourceYesNo

Status Codes

Success (2xx):

  • 200 OK - Request succeeded
  • 201 Created - Resource created (return Location header)
  • 204 No Content - Success with no response body

Client Error (4xx):

  • 400 Bad Request - Malformed request
  • 401 Unauthorized - Authentication required
  • 403 Forbidden - No permission
  • 404 Not Found - Resource doesn't exist
  • 409 Conflict - State conflict
  • 422 Unprocessable Entity - Validation failed
  • 429 Too Many Requests - Rate limited

Server Error (5xx):

  • 500 Internal Server Error - Unexpected error
  • 503 Service Unavailable - Temporary outage

Response Format

Successful response:

{
  "data": {
    "id": "123",
    "name": "John Doe",
    "email": "john@example.com"
  }
}

Collection response:

{
  "data": [
    { "id": "1", "name": "Item 1" },
    { "id": "2", "name": "Item 2" }
  ],
  "meta": {
    "page": 1,
    "perPage": 20,
    "total": 100,
    "totalPages": 5
  },
  "links": {
    "self": "/items?page=1",
    "next": "/items?page=2",
    "prev": null
  }
}

Error response:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Must be a valid email address"
      }
    ]
  }
}

Pagination

Offset-based (simple, but slow on large datasets):

GET /users?page=2&perPage=20
GET /users?offset=40&limit=20

Cursor-based (efficient, recommended):

GET /users?cursor=eyJpZCI6MTIzfQ&limit=20

Response includes next cursor:

{
  "data": [...],
  "meta": {
    "nextCursor": "eyJpZCI6MTQzfQ",
    "hasMore": true
  }
}

Filtering, Sorting, Fields

Filtering:

GET /users?status=active
GET /users?created_after=2024-01-01
GET /users?role=admin,moderator

Sorting:

GET /users?sort=name
GET /users?sort=-created_at         # Descending
GET /users?sort=status,-created_at  # Multiple fields

Field selection:

GET /users?fields=id,name,email
GET /users?include=orders,profile

Versioning

URL path (recommended):

/api/v1/users
/api/v2/users

Header:

Accept: application/vnd.api+json;version=2

OpenAPI Specification

openapi: 3.0.3
info:
  title: My API
  version: 1.0.0
  description: API for managing users

servers:
  - url: https://api.example.com/v1

paths:
  /users:
    get:
      summary: List users
      tags: [Users]
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        "200":
          description: List of users
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"

components:
  schemas:
    User:
      type: object
      required: [id, email]
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string

Authentication

API Keys (simple, for server-to-server):

Authorization: Api-Key YOUR_API_KEY

Bearer Tokens (JWT, OAuth):

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Include in OpenAPI:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

security:
  - bearerAuth: []

Rate Limiting

Include headers in responses:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200

Return 429 Too Many Requests when exceeded.


Security Checklist

  • HTTPS only
  • Authentication on protected routes
  • Input validation
  • Output encoding
  • Rate limiting
  • CORS configuration
  • No sensitive data in URLs
  • Audit logging

gRPC and Protocol Buffers

Proto Definition

syntax = "proto3";
package user.v1;

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc StreamUpdates(StreamRequest) returns (stream UserUpdate);
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  google.protobuf.Timestamp created_at = 4;
}

message GetUserRequest {
  string id = 1;
}

message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

message ListUsersResponse {
  repeated User users = 1;
  string next_page_token = 2;
}

When to Use gRPC vs REST

FactorgRPCREST
PerformanceBinary, fastJSON, human-readable
StreamingBidirectionalSSE/WebSocket workaround
Type safetyProto generates typesOpenAPI + codegen
BrowserNeeds gRPC-Web proxyNative
ToolingProtoc, BufSwagger, Postman
Best forService-to-service, streamingPublic APIs, web clients

tRPC for TypeScript

// server/router.ts
import { router, publicProcedure, protectedProcedure } from './trpc';
import { z } from 'zod';

export const appRouter = router({
  user: router({
    get: publicProcedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input }) => {
        return db.user.findUnique({ where: { id: input.id } });
      }),
    create: protectedProcedure
      .input(z.object({
        name: z.string().min(1),
        email: z.string().email(),
      }))
      .mutation(async ({ input, ctx }) => {
        return db.user.create({ data: { ...input, createdBy: ctx.userId } });
      }),
  }),
});

export type AppRouter = typeof appRouter;

// client.ts - Full type inference, no codegen
const user = trpc.user.get.useQuery({ id: '123' });
const createUser = trpc.user.create.useMutation();

tRPC is ideal for monorepo full-stack TypeScript apps where client and server share the same codebase.


Webhook Design Patterns

Webhook Payload

{
  "id": "evt_abc123",
  "type": "order.completed",
  "created_at": "2025-01-15T10:30:00Z",
  "data": {
    "order_id": "ord_456",
    "total": 99.99,
    "currency": "USD"
  }
}

Signature Verification

// Sign webhooks with HMAC-SHA256
import crypto from 'crypto';

function signWebhook(payload: string, secret: string): string {
  return crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
}

// Verify on receiving end
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = signWebhook(payload, secret);
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected),
  );
}

Retry Strategy

Attempt 1: Immediately
Attempt 2: After 1 minute
Attempt 3: After 5 minutes
Attempt 4: After 30 minutes
Attempt 5: After 2 hours
Attempt 6: After 24 hours (final)

Failed webhooks: log, alert, manual retry UI

Idempotency

// Include idempotency key in webhook
// Receivers should deduplicate based on event ID
async function handleWebhook(event: WebhookEvent) {
  // Check if already processed
  const existing = await db.processedEvents.findUnique({
    where: { eventId: event.id },
  });
  if (existing) return { status: 'already_processed' };

  // Process and record
  await db.$transaction([
    processEvent(event),
    db.processedEvents.create({ data: { eventId: event.id } }),
  ]);
}

API Versioning Strategies

StrategyExampleProsCons
URL path/api/v1/usersExplicit, easy to routeURL pollution
Query parameter/api/users?version=1Optional parameterEasy to miss
HeaderAccept: application/vnd.api.v1Clean URLsHidden, harder to test
Content negotiationAccept: application/json;v=2Standards-basedComplex to implement

Recommendation: URL path versioning for simplicity. Only bump major versions for breaking changes. Use additive, non-breaking changes within a version.

api-design - 适用于 Claude Code 与 Cursor 的 AI 智能体 Skill | Agent Skills