Skip to content
api-design logo

api-design

REST API, GraphQL, and API design patterns and best practices

SKILL.md

Full skill instructions

API Design Skill

Comprehensive patterns and best practices for designing REST APIs, GraphQL APIs, and API-first development.

What I Know

REST API Design Principles

Resource Naming

# Good - nouns, plural, lowercase, hyphenated
GET /​users
GET /​users/​{id}
GET /​users/​{id}/​posts
GET /​blog-posts
GET /​order-items

# Bad - verbs, singular, camelCase
GET /​getUsers
GET /​user
GET /​getUserPosts
GET /​blogPosts

HTTP Methods

MethodUsageIdempotentRequest BodyResponse
GETRead resource(s)YesNoResource(s)
POSTCreate resourceNoYesCreated resource
PUTReplace resourceYesYesUpdated resource
PATCHPartial updateNo*YesUpdated resource
DELETERemove resourceYesNoEmpty or confirmation

Status Codes

# Success
200 OK              - Successful GET, PUT, PATCH
201 Created         - Successful POST
204 No Content      - Successful DELETE

# Client Errors
400 Bad Request     - Invalid request body/​params
401 Unauthorized    - Missing/​invalid authentication
403 Forbidden       - Valid auth, insufficient permissions
404 Not Found       - Resource doesn't exist
409 Conflict        - Resource state conflict (duplicate)
422 Unprocessable   - Validation errors

# Server Errors
500 Internal Error  - Unexpected server error
502 Bad Gateway     - Upstream service error
503 Service Unavailable - Temporary overload

Standardized Response Format

Success Response

{
  "success": true,
  "data": {
    "id": 123,
    "email": "[email protected]",
    "name": "John Doe"
  }
}

List Response with Pagination

{
  "success": true,
  "data": [
    { "id": 1, "name": "Item 1" },
    { "id": 2, "name": "Item 2" }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5,
    "hasMore": true
  }
}

Error Response

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "password",
        "message": "Password must be at least 8 characters"
      }
    ]
  }
}

Pagination Patterns

Offset-Based (Simple)

GET /​users?page=2&limit=20

Response:
{
  "data": [...],
  "meta": {
    "page": 2,
    "limit": 20,
    "total": 150,
    "totalPages": 8
  }
}

Cursor-Based (Scalable)

GET /​users?cursor=abc123&limit=20

Response:
{
  "data": [...],
  "meta": {
    "limit": 20,
    "nextCursor": "def456",
    "hasMore": true
  }
}

Filtering & Sorting

Query Parameters

# Filtering
GET /​users?status=active
GET /​users?role=admin&status=active
GET /​products?price_min=10&price_max=100
GET /​posts?created_after=2024-01-01

# Sorting
GET /​users?sort=name        # Ascending
GET /​users?sort=-createdAt  # Descending
GET /​users?sort=name,-createdAt  # Multiple

# Field Selection
GET /​users?fields=id,name,email

# Search
GET /​users?search=john
GET /​products?q=laptop

API Versioning

URI Versioning (Recommended)

GET /​api/​v1/​users
GET /​api/​v2/​users

Header Versioning

GET /​api/​users
Accept: application/​vnd.api+json;version=1

Query Parameter

GET /​api/​users?version=1

Authentication Patterns

JWT Bearer Token

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

API Key

# Header
X-API-Key: your-api-key

# Query (less secure)
GET /​api/​users?api_key=your-api-key

OAuth 2.0 Flows

# Authorization Code (Web apps)
# Client Credentials (Server-to-server)
# PKCE (Mobile/​SPA)

Rate Limiting

Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200
Retry-After: 60

429 Response

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests",
    "retryAfter": 60
  }
}

OpenAPI/​Swagger 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
    description: Production

paths:
  /​users:
    get:
      summary: List users
      tags:
        - Users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/​json:
              schema:
                $ref: '#/​components/​schemas/​UserList'
        '401':
          $ref: '#/​components/​responses/​Unauthorized'

    post:
      summary: Create user
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/​json:
            schema:
              $ref: '#/​components/​schemas/​CreateUser'
      responses:
        '201':
          description: Created
          content:
            application/​json:
              schema:
                $ref: '#/​components/​schemas/​UserResponse'
        '400':
          $ref: '#/​components/​responses/​BadRequest'
        '409':
          $ref: '#/​components/​responses/​Conflict'

  /​users/​{id}:
    get:
      summary: Get user by ID
      tags:
        - Users
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Success
          content:
            application/​json:
              schema:
                $ref: '#/​components/​schemas/​UserResponse'
        '404':
          $ref: '#/​components/​responses/​NotFound'

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

    CreateUser:
      type: object
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 2
          maxLength: 100
        password:
          type: string
          minLength: 8
      required:
        - email
        - name
        - password

    UserResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/​components/​schemas/​User'

    UserList:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/​components/​schemas/​User'
        meta:
          $ref: '#/​components/​schemas/​Pagination'

    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer

    Error:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: array
              items:
                type: object

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/​json:
          schema:
            $ref: '#/​components/​schemas/​Error'
    Unauthorized:
      description: Unauthorized
      content:
        application/​json:
          schema:
            $ref: '#/​components/​schemas/​Error'
    NotFound:
      description: Not Found
      content:
        application/​json:
          schema:
            $ref: '#/​components/​schemas/​Error'
    Conflict:
      description: Conflict
      content:
        application/​json:
          schema:
            $ref: '#/​components/​schemas/​Error'

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

security:
  - bearerAuth: []

GraphQL Patterns

Schema Design

type Query {
  user(id: ID!): User
  users(
    page: Int = 1
    limit: Int = 20
    filter: UserFilter
  ): UserConnection!
}

type Mutation {
  createUser(input: CreateUserInput!): UserPayload!
  updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
  deleteUser(id: ID!): DeletePayload!
}

type User {
  id: ID!
  email: String!
  name: String!
  posts(first: Int): [Post!]!
  createdAt: DateTime!
}

input CreateUserInput {
  email: String!
  name: String!
  password: String!
}

input UpdateUserInput {
  email: String
  name: String
}

input UserFilter {
  status: UserStatus
  role: UserRole
  search: String
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

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

type UserPayload {
  user: User
  errors: [Error!]
}

type DeletePayload {
  success: Boolean!
  errors: [Error!]
}

type Error {
  field: String
  message: String!
}

Resolver Patterns

const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      return context.dataSources.users.findById(id)
    },
    users: async (_, { page, limit, filter }, context) => {
      return context.dataSources.users.findAll({ page, limit, filter })
    },
  },
  Mutation: {
    createUser: async (_, { input }, context) => {
      try {
        const user = await context.dataSources.users.create(input)
        return { user, errors: [] }
      } catch (error) {
        return { user: null, errors: [{ message: error.message }] }
      }
    },
  },
  User: {
    posts: async (user, { first }, context) => {
      return context.dataSources.posts.findByUserId(user.id, { first })
    },
  },
}

Error Handling Best Practices

Error Codes

enum ErrorCode {
  // Authentication
  INVALID_TOKEN = 'INVALID_TOKEN',
  TOKEN_EXPIRED = 'TOKEN_EXPIRED',
  UNAUTHORIZED = 'UNAUTHORIZED',

  // Authorization
  FORBIDDEN = 'FORBIDDEN',
  INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',

  // Validation
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  INVALID_INPUT = 'INVALID_INPUT',

  // Resource
  NOT_FOUND = 'NOT_FOUND',
  ALREADY_EXISTS = 'ALREADY_EXISTS',
  CONFLICT = 'CONFLICT',

  // Rate Limiting
  RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',

  // Server
  INTERNAL_ERROR = 'INTERNAL_ERROR',
  SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
}

HATEOAS (Hypermedia)

{
  "success": true,
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "[email protected]"
  },
  "links": {
    "self": "/​api/​v1/​users/​123",
    "posts": "/​api/​v1/​users/​123/​posts",
    "update": "/​api/​v1/​users/​123",
    "delete": "/​api/​v1/​users/​123"
  }
}

Caching Headers

# Cache for 1 hour
Cache-Control: max-age=3600, public

# No cache
Cache-Control: no-store, no-cache, must-revalidate

# ETag for conditional requests
ETag: "abc123"
If-None-Match: "abc123"

# Last-Modified
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

Security Best Practices

  1. Always use HTTPS
  2. Validate all input - Never trust client data
  3. Rate limit endpoints - Prevent abuse
  4. Use proper authentication - JWT, OAuth 2.0
  5. Implement authorization - Check permissions
  6. Sanitize output - Prevent XSS
  7. Use parameterized queries - Prevent SQL injection
  8. Log security events - Audit trail
  9. Don't expose internals - Generic error messages
  10. Set security headers - CORS, CSP, etc.

API Documentation

Good Documentation Includes:

  • Authentication guide
  • Request/​response examples
  • Error code reference
  • Rate limiting info
  • Changelog/​versioning
  • SDKs and code samples
  • Interactive playground (Swagger UI)

Common Pitfalls

  1. Inconsistent naming - Stick to one convention
  2. Missing validation - Always validate input
  3. No versioning - Version from the start
  4. Poor error messages - Be helpful, not cryptic
  5. No rate limiting - Protect from abuse
  6. Exposing internals - Don't leak stack traces
  7. Ignoring caching - Set proper cache headers
  8. No pagination - Always paginate lists
  9. Breaking changes - Use versioning properly
  10. Missing documentation - Document everything

Part of SuperAI GitHub - Centralized OpenCode Configuration