Files
2025-11-30 08:53:58 +08:00

12 KiB

API Designer

Description: Design clean, consistent, and developer-friendly RESTful and GraphQL APIs

Core Principles

You are an API design expert who creates intuitive, well-documented, and scalable APIs that developers love to use.

RESTful API Design

1. Resource-Oriented URLs

✅ Good: Noun-based, resource-oriented
GET    /api/users              # List users
GET    /api/users/123          # Get specific user
POST   /api/users              # Create user
PUT    /api/users/123          # Update user (full)
PATCH  /api/users/123          # Update user (partial)
DELETE /api/users/123          # Delete user

GET    /api/users/123/posts    # Get user's posts
POST   /api/users/123/posts    # Create post for user

❌ Bad: Verb-based, action-oriented
GET    /api/getUsers
POST   /api/createUser
POST   /api/updateUser
POST   /api/deleteUser

2. HTTP Methods (Verbs) Properly

GET    - Retrieve resources (safe, idempotent)
POST   - Create new resources
PUT    - Replace entire resource (idempotent)
PATCH  - Partial update
DELETE - Remove resource (idempotent)

Safe: No side effects (can cache)
Idempotent: Same request = same result (can retry safely)

3. HTTP Status Codes

✅ Use appropriate status codes

Success (2xx):
200 OK              - Successful GET, PUT, PATCH, DELETE
201 Created         - Successful POST (resource created)
204 No Content      - Successful DELETE (no response body)

Client Errors (4xx):
400 Bad Request     - Invalid syntax, validation error
401 Unauthorized    - Authentication required
403 Forbidden       - Authenticated but not authorized
404 Not Found       - Resource doesn't exist
409 Conflict        - Conflict with current state (e.g., duplicate)
422 Unprocessable   - Validation error (semantic)
429 Too Many Requests - Rate limiting

Server Errors (5xx):
500 Internal Server Error  - Generic server error
502 Bad Gateway           - Upstream service error
503 Service Unavailable   - Temporary unavailability

❌ Avoid: Always returning 200 with error in body

4. Request/Response Format

// ✅ Good: Consistent structure

// Success response
{
  "data": {
    "id": "123",
    "name": "John Doe",
    "email": "john@example.com",
    "createdAt": "2025-01-15T10:30:00Z"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "version": "1.0"
  }
}

// List response with pagination
{
  "data": [
    { "id": "1", "name": "User 1" },
    { "id": "2", "name": "User 2" }
  ],
  "meta": {
    "page": 1,
    "perPage": 20,
    "total": 150,
    "totalPages": 8
  },
  "links": {
    "self": "/api/users?page=1",
    "next": "/api/users?page=2",
    "last": "/api/users?page=8"
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      }
    ]
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "requestId": "req_abc123"
  }
}

5. Filtering, Sorting, Pagination

✅ Good: Query parameters for operations

// Filtering
GET /api/products?category=electronics&minPrice=100&maxPrice=500

// Sorting
GET /api/products?sort=price            # Ascending
GET /api/products?sort=-price           # Descending
GET /api/products?sort=category,-price  # Multi-field

// Pagination (offset-based)
GET /api/products?page=2&perPage=20

// Pagination (cursor-based for large datasets)
GET /api/products?cursor=abc123&limit=20

// Field selection (sparse fieldsets)
GET /api/users?fields=id,name,email

// Search
GET /api/products?q=laptop

6. Versioning

✅ Option 1: URL path (most common)
GET /api/v1/users
GET /api/v2/users

✅ Option 2: Header
GET /api/users
Headers: API-Version: 1

✅ Option 3: Content negotiation
GET /api/users
Accept: application/vnd.myapi.v1+json

❌ Bad: No versioning (breaking changes break clients)

API Design Patterns

1. Nested Resources

// One level is fine
GET /api/users/123/posts

// Multiple levels get messy
❌ GET /api/users/123/posts/456/comments/789

// Better: Top-level access with filters
✅ GET /api/comments/789
✅ GET /api/comments?postId=456
✅ GET /api/comments?userId=123

2. Bulk Operations

// Bulk create
POST /api/users/bulk
{
  "data": [
    { "name": "User 1", "email": "user1@example.com" },
    { "name": "User 2", "email": "user2@example.com" }
  ]
}

// Bulk update
PATCH /api/users/bulk
{
  "data": [
    { "id": "1", "name": "Updated 1" },
    { "id": "2", "name": "Updated 2" }
  ]
}

// Response includes success and failures
{
  "data": {
    "successful": [
      { "id": "1", "name": "Updated 1" }
    ],
    "failed": [
      {
        "id": "2",
        "error": "Validation error",
        "details": "Name too long"
      }
    ]
  }
}

3. Actions on Resources

// When REST verbs don't fit, use sub-resources

// ❌ Avoid creating new endpoints
POST /api/approveDocument
POST /api/cancelOrder

// ✅ Better: Actions as sub-resources
POST /api/documents/123/approve
POST /api/orders/456/cancel

// Or state transition
PATCH /api/documents/123
{ "status": "approved" }

PATCH /api/orders/456
{ "status": "cancelled" }

4. Idempotency

// For retry safety, use idempotency keys
POST /api/payments
Headers:
  Idempotency-Key: unique-request-id-123
Body:
  { "amount": 100, "currency": "USD" }

// Multiple identical requests = same result
// Server tracks idempotency key and returns cached response

5. Rate Limiting

// Include rate limit info in headers
Headers:
  X-RateLimit-Limit: 1000          # Max requests per window
  X-RateLimit-Remaining: 234       # Requests remaining
  X-RateLimit-Reset: 1642262400    # When limit resets (Unix timestamp)

// When exceeded
Status: 429 Too Many Requests
Headers:
  Retry-After: 3600                # Seconds until retry
Body:
  {
    "error": {
      "code": "RATE_LIMIT_EXCEEDED",
      "message": "Too many requests, try again later"
    }
  }

Security Best Practices

1. Authentication & Authorization

// JWT Bearer Token (most common)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

// API Key (for server-to-server)
X-API-Key: your-api-key-here

// OAuth 2.0 (for third-party access)
Authorization: Bearer {access_token}

// Always use HTTPS in production

2. Input Validation

// Validate all inputs
function createUser(req, res) {
  const { email, password, name } = req.body;

  // Type validation
  if (typeof email !== 'string') {
    return res.status(400).json({
      error: { code: 'INVALID_TYPE', field: 'email' }
    });
  }

  // Format validation
  if (!isValidEmail(email)) {
    return res.status(400).json({
      error: { code: 'INVALID_FORMAT', field: 'email' }
    });
  }

  // Length validation
  if (password.length < 8) {
    return res.status(400).json({
      error: { code: 'TOO_SHORT', field: 'password' }
    });
  }

  // Business rule validation
  if (await userExists(email)) {
    return res.status(409).json({
      error: { code: 'DUPLICATE', field: 'email' }
    });
  }

  // Sanitize inputs
  const sanitizedName = sanitizeHtml(name);

  // Continue with creation...
}

3. Prevent Common Vulnerabilities

✅ Use parameterized queries (prevent SQL injection)
✅ Sanitize HTML inputs (prevent XSS)
✅ Validate content types
✅ Implement CORS properly
✅ Use CSRF tokens for state-changing operations
✅ Rate limit all endpoints
✅ Log security events

Documentation

OpenAPI/Swagger Example

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0

paths:
  /api/users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: perPage
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'

    post:
      summary: Create user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - name
              properties:
                email:
                  type: string
                  format: email
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time

Testing APIs

1. Test Structure

describe('POST /api/users', () => {
  describe('Success Cases', () => {
    it('should create user with valid data', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ email: 'test@example.com', name: 'Test' })
        .expect(201);

      expect(response.body.data).toHaveProperty('id');
      expect(response.body.data.email).toBe('test@example.com');
    });
  });

  describe('Validation Errors', () => {
    it('should return 400 for invalid email', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ email: 'invalid', name: 'Test' })
        .expect(400);

      expect(response.body.error.code).toBe('VALIDATION_ERROR');
    });

    it('should return 400 for missing required field', async () => {
      await request(app)
        .post('/api/users')
        .send({ name: 'Test' })
        .expect(400);
    });
  });

  describe('Business Logic Errors', () => {
    it('should return 409 for duplicate email', async () => {
      await createUser({ email: 'existing@example.com' });

      await request(app)
        .post('/api/users')
        .send({ email: 'existing@example.com', name: 'Test' })
        .expect(409);
    });
  });

  describe('Authorization', () => {
    it('should return 401 without auth token', async () => {
      await request(app)
        .post('/api/users')
        .send({ email: 'test@example.com', name: 'Test' })
        .expect(401);
    });
  });
});

Performance Optimization

1. Caching

// Cache headers
Cache-Control: public, max-age=3600    # Cache for 1 hour
ETag: "abc123"                          # Version identifier

// Conditional requests
If-None-Match: "abc123"                # Client sends ETag
Response: 304 Not Modified             # If unchanged

// Vary header (cache based on header)
Vary: Accept-Language, Authorization

2. Compression

// Enable gzip/brotli compression
Content-Encoding: gzip

3. Field Selection

// Only return requested fields
GET /api/users?fields=id,name,email

// Reduces payload size and database load

When to Use This Skill

  • Designing new APIs from scratch
  • Refactoring existing APIs
  • Reviewing API specifications
  • Implementing API endpoints
  • Writing API documentation
  • Debugging API issues

Remember: Good API design is about consistency, predictability, and developer experience. Make your API intuitive and well-documented!