Skip to content
Home / Skills / Api Design / Documentation
AP

Documentation

Api Design core v1.0.0

API Documentation

Overview

Good API documentation enables developers to integrate quickly and correctly. This skill covers OpenAPI/Swagger specifications, documentation best practices, and developer experience.


Key Concepts

Documentation Components

┌─────────────────────────────────────────────────────────────┐
│              API Documentation Components                    │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. Reference Documentation (OpenAPI):                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • Endpoints and methods                            │   │
│  │  • Request/response schemas                          │   │
│  │  • Authentication requirements                       │   │
│  │  • Error codes and responses                        │   │
│  │  • Query parameters and headers                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  2. Conceptual Documentation:                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • Getting started guide                            │   │
│  │  • Authentication overview                          │   │
│  │  • Rate limiting policies                           │   │
│  │  • Pagination conventions                           │   │
│  │  • Error handling guide                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  3. Tutorials and Examples:                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • Use case walkthroughs                            │   │
│  │  • Code samples in multiple languages               │   │
│  │  • Interactive examples (try it)                    │   │
│  │  • SDKs and client libraries                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  4. Changelog and Migration:                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • Version history                                  │   │
│  │  • Breaking changes                                  │   │
│  │  • Deprecation notices                              │   │
│  │  • Migration guides                                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Best Practices

1. Document Every Endpoint

Include all parameters, responses, and errors.

2. Provide Runnable Examples

Show complete, copy-paste-ready code.

3. Document Error Scenarios

Include all possible error responses.

4. Keep Documentation in Sync

Generate from code or validate continuously.

5. Include Authentication Details

Show how to authenticate in every example.


Code Examples

Example 1: OpenAPI Specification

openapi: 3.1.0
info:
  title: Order Management API
  description: |
    API for managing customer orders.
    
    ## Authentication
    All endpoints require a Bearer token in the Authorization header.
    
    ## Rate Limiting
    - Standard tier: 100 requests/minute
    - Premium tier: 1000 requests/minute
  version: 2.0.0
  contact:
    name: API Support
    email: api-support@example.com
    url: https://developer.example.com/support
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0

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

tags:
  - name: Orders
    description: Order management operations
  - name: Customers
    description: Customer management operations

paths:
  /orders:
    get:
      tags: [Orders]
      summary: List orders
      description: |
        Retrieve a paginated list of orders with optional filtering.
        
        **Permissions required:** `orders:read`
      operationId: listOrders
      parameters:
        - name: status
          in: query
          description: Filter by order status
          schema:
            $ref: '#/components/schemas/OrderStatus'
        - name: customerId
          in: query
          description: Filter by customer ID
          schema:
            type: string
            format: uuid
        - name: page
          in: query
          description: Page number (0-indexed)
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: size
          in: query
          description: Page size
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderListResponse'
              example:
                data:
                  - id: "ord_123abc"
                    status: "pending"
                    total: 99.99
                    createdAt: "2024-01-15T10:30:00Z"
                pagination:
                  page: 0
                  size: 20
                  totalElements: 150
                  totalPages: 8
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

    post:
      tags: [Orders]
      summary: Create order
      description: |
        Create a new order for a customer.
        
        **Permissions required:** `orders:write`
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
            example:
              customerId: "cust_456def"
              items:
                - productId: "prod_789ghi"
                  quantity: 2
                - productId: "prod_012jkl"
                  quantity: 1
              shippingAddress:
                street: "123 Main St"
                city: "San Francisco"
                state: "CA"
                postalCode: "94102"
                country: "US"
      responses:
        '201':
          description: Order created
          headers:
            Location:
              description: URL of created order
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/ValidationError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT token obtained from the authentication endpoint.
        
        Example: `Authorization: Bearer eyJhbGciOiJIUzI1NiIs...`

  schemas:
    Order:
      type: object
      required:
        - id
        - status
        - customerId
        - items
        - total
        - createdAt
      properties:
        id:
          type: string
          description: Unique order identifier
          example: "ord_123abc"
        status:
          $ref: '#/components/schemas/OrderStatus'
        customerId:
          type: string
          format: uuid
          description: Customer who placed the order
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        total:
          type: number
          format: decimal
          description: Total order amount
          example: 99.99
        createdAt:
          type: string
          format: date-time
          description: When the order was created
        updatedAt:
          type: string
          format: date-time
          description: When the order was last updated

    OrderStatus:
      type: string
      enum: [pending, confirmed, shipped, delivered, cancelled]
      description: |
        Order status:
        - `pending`: Order received, awaiting confirmation
        - `confirmed`: Order confirmed and processing
        - `shipped`: Order shipped to customer
        - `delivered`: Order delivered
        - `cancelled`: Order cancelled

    ProblemDetail:
      type: object
      description: RFC 7807 Problem Details format
      properties:
        type:
          type: string
          format: uri
          description: URI identifying the problem type
        title:
          type: string
          description: Short, human-readable summary
        status:
          type: integer
          description: HTTP status code
        detail:
          type: string
          description: Human-readable explanation
        instance:
          type: string
          format: uri
          description: URI of the specific occurrence
        traceId:
          type: string
          description: Request trace ID for debugging

  responses:
    Unauthorized:
      description: Authentication required or invalid token
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'
          example:
            type: "https://api.example.com/errors/unauthorized"
            title: "Unauthorized"
            status: 401
            detail: "Bearer token is missing or invalid"

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          description: Seconds until rate limit resets
          schema:
            type: integer
        X-RateLimit-Limit:
          description: Rate limit ceiling
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Remaining requests in window
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

security:
  - bearerAuth: []

Example 2: SpringDoc OpenAPI Configuration

@Configuration
public class OpenApiConfig {
    
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("Order Management API")
                .version("2.0.0")
                .description("API for managing customer orders")
                .contact(new Contact()
                    .name("API Support")
                    .email("api-support@example.com")
                    .url("https://developer.example.com"))
                .license(new License()
                    .name("Apache 2.0")
                    .url("https://www.apache.org/licenses/LICENSE-2.0")))
            .externalDocs(new ExternalDocumentation()
                .description("Full Documentation")
                .url("https://developer.example.com/docs"))
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
            .components(new Components()
                .addSecuritySchemes("bearerAuth", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")
                    .description("JWT authentication token")));
    }
    
    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
            .group("public")
            .pathsToMatch("/api/v2/**")
            .addOperationCustomizer((operation, handlerMethod) -> {
                // Add common headers
                operation.addParametersItem(new Parameter()
                    .in("header")
                    .name("X-Request-ID")
                    .description("Client-generated request ID for tracing")
                    .schema(new StringSchema()));
                return operation;
            })
            .build();
    }
}

@RestController
@RequestMapping("/api/v2/orders")
@Tag(name = "Orders", description = "Order management operations")
public class OrderController {
    
    @Operation(
        summary = "Create a new order",
        description = """
            Create a new order for a customer.
            
            The order will be created in 'pending' status and needs to be confirmed
            before processing begins.
            
            **Permissions required:** `orders:write`
            """,
        responses = {
            @ApiResponse(
                responseCode = "201",
                description = "Order created successfully",
                headers = @Header(
                    name = "Location",
                    description = "URL of the created order"
                ),
                content = @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = OrderResponse.class),
                    examples = @ExampleObject(
                        name = "Created order",
                        value = """
                            {
                              "id": "ord_123abc",
                              "status": "pending",
                              "total": 99.99,
                              "createdAt": "2024-01-15T10:30:00Z"
                            }
                            """
                    )
                )
            ),
            @ApiResponse(
                responseCode = "422",
                description = "Validation failed",
                content = @Content(
                    mediaType = "application/problem+json",
                    schema = @Schema(implementation = ProblemDetail.class)
                )
            )
        }
    )
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(
                description = "Order details",
                required = true,
                content = @Content(
                    schema = @Schema(implementation = CreateOrderRequest.class),
                    examples = @ExampleObject(
                        name = "Example order",
                        value = """
                            {
                              "customerId": "cust_456",
                              "items": [
                                {"productId": "prod_789", "quantity": 2}
                              ]
                            }
                            """
                    )
                )
            )
            @Valid @RequestBody CreateOrderRequest request) {
        // Implementation
    }
    
    @Operation(
        summary = "Get order by ID",
        parameters = {
            @Parameter(
                name = "id",
                description = "Order ID",
                required = true,
                example = "ord_123abc"
            )
        }
    )
    @GetMapping("/{id}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable String id) {
        // Implementation
    }
}

Example 3: Request/Response Documentation

@Schema(description = "Request to create a new order")
@Data
public class CreateOrderRequest {
    
    @Schema(
        description = "Customer ID placing the order",
        example = "cust_456def",
        requiredMode = Schema.RequiredMode.REQUIRED
    )
    @NotNull
    private String customerId;
    
    @Schema(
        description = "Items to include in the order",
        minLength = 1
    )
    @NotEmpty
    @Valid
    private List<OrderItemRequest> items;
    
    @Schema(description = "Shipping address for the order")
    @Valid
    private AddressRequest shippingAddress;
    
    @Schema(
        description = "Special instructions for the order",
        example = "Please leave at front door",
        maxLength = 500
    )
    @Size(max = 500)
    private String notes;
}

@Schema(description = "Order item")
@Data
public class OrderItemRequest {
    
    @Schema(
        description = "Product ID",
        example = "prod_789ghi",
        requiredMode = Schema.RequiredMode.REQUIRED
    )
    @NotNull
    private String productId;
    
    @Schema(
        description = "Quantity to order",
        minimum = "1",
        maximum = "100",
        example = "2",
        requiredMode = Schema.RequiredMode.REQUIRED
    )
    @Min(1)
    @Max(100)
    private Integer quantity;
}

@Schema(description = "Order response")
@Data
public class OrderResponse {
    
    @Schema(
        description = "Unique order identifier",
        example = "ord_123abc"
    )
    private String id;
    
    @Schema(
        description = "Current order status",
        example = "pending"
    )
    private OrderStatus status;
    
    @Schema(
        description = "Order total in customer's currency",
        example = "99.99"
    )
    private BigDecimal total;
    
    @Schema(
        description = "Currency code",
        example = "USD"
    )
    private String currency;
    
    @Schema(description = "Order line items")
    private List<OrderItemResponse> items;
    
    @Schema(
        description = "When the order was created",
        example = "2024-01-15T10:30:00Z"
    )
    private Instant createdAt;
    
    @Schema(description = "HATEOAS links for available actions")
    private Map<String, Link> _links;
}

Example 4: Interactive Documentation Examples

// Code samples for documentation
public class ApiExamples {
    
    /**
     * Generate code samples for different languages
     */
    public Map<String, String> getCodeSamples(String endpoint, String method, Object requestBody) {
        Map<String, String> samples = new LinkedHashMap<>();
        
        samples.put("curl", generateCurlExample(endpoint, method, requestBody));
        samples.put("python", generatePythonExample(endpoint, method, requestBody));
        samples.put("javascript", generateJavaScriptExample(endpoint, method, requestBody));
        samples.put("java", generateJavaExample(endpoint, method, requestBody));
        
        return samples;
    }
    
    private String generateCurlExample(String endpoint, String method, Object body) {
        StringBuilder curl = new StringBuilder();
        curl.append("curl -X ").append(method).append(" \\\n");
        curl.append("  'https://api.example.com/v2").append(endpoint).append("' \\\n");
        curl.append("  -H 'Authorization: Bearer YOUR_TOKEN' \\\n");
        curl.append("  -H 'Content-Type: application/json'");
        
        if (body != null) {
            curl.append(" \\\n  -d '").append(toJson(body)).append("'");
        }
        
        return curl.toString();
    }
    
    private String generatePythonExample(String endpoint, String method, Object body) {
        return """
            import requests
            
            response = requests.%s(
                'https://api.example.com/v2%s',
                headers={
                    'Authorization': 'Bearer YOUR_TOKEN',
                    'Content-Type': 'application/json'
                },
                json=%s
            )
            
            print(response.json())
            """.formatted(method.toLowerCase(), endpoint, toPythonDict(body));
    }
    
    private String generateJavaScriptExample(String endpoint, String method, Object body) {
        return """
            const response = await fetch('https://api.example.com/v2%s', {
              method: '%s',
              headers: {
                'Authorization': 'Bearer YOUR_TOKEN',
                'Content-Type': 'application/json'
              },
              body: JSON.stringify(%s)
            });
            
            const data = await response.json();
            console.log(data);
            """.formatted(endpoint, method, toJson(body));
    }
}

// Endpoint to generate live examples
@RestController
@RequestMapping("/api/docs")
public class DocumentationController {
    
    private final ApiExamples examples;
    
    @GetMapping("/examples/{operation}")
    public ResponseEntity<Map<String, String>> getExamples(@PathVariable String operation) {
        // Return code examples for the operation
        return switch (operation) {
            case "create-order" -> ResponseEntity.ok(
                examples.getCodeSamples("/orders", "POST", 
                    Map.of("customerId", "cust_456", "items", List.of(
                        Map.of("productId", "prod_789", "quantity", 2)
                    ))
                )
            );
            case "get-order" -> ResponseEntity.ok(
                examples.getCodeSamples("/orders/ord_123", "GET", null)
            );
            default -> ResponseEntity.notFound().build();
        };
    }
}

Example 5: Changelog and Migration Documentation

@RestController
@RequestMapping("/api/docs")
public class ChangelogController {
    
    @GetMapping("/changelog")
    public ResponseEntity<Changelog> getChangelog() {
        return ResponseEntity.ok(new Changelog(List.of(
            new ChangelogEntry(
                "2.1.0",
                LocalDate.of(2024, 2, 1),
                List.of(
                    new Change(ChangeType.ADDED, 
                        "Added `metadata` field to Order response"),
                    new Change(ChangeType.ADDED, 
                        "New endpoint: GET /orders/{id}/history"),
                    new Change(ChangeType.DEPRECATED, 
                        "`notes` field deprecated, use `metadata.notes` instead")
                )
            ),
            new ChangelogEntry(
                "2.0.0",
                LocalDate.of(2024, 1, 1),
                List.of(
                    new Change(ChangeType.BREAKING, 
                        "User.name split into firstName and lastName"),
                    new Change(ChangeType.BREAKING, 
                        "Renamed /users/{id}/orders to /orders?customerId={id}"),
                    new Change(ChangeType.ADDED, 
                        "Added address field to User response"),
                    new Change(ChangeType.REMOVED, 
                        "Removed legacy XML response format")
                )
            )
        )));
    }
    
    @GetMapping("/migration/{from}/{to}")
    public ResponseEntity<MigrationGuide> getMigrationGuide(
            @PathVariable String from,
            @PathVariable String to) {
        
        if ("1".equals(from) && "2".equals(to)) {
            return ResponseEntity.ok(new MigrationGuide(
                "v1", "v2",
                """
                    # Migration Guide: v1 to v2
                    
                    ## Breaking Changes
                    
                    ### 1. User Name Field Split
                    
                    **Before (v1):**
                    ```json
                    { "name": "John Doe" }
                    ```
                    
                    **After (v2):**
                    ```json
                    { "firstName": "John", "lastName": "Doe" }
                    ```
                    
                    **Migration steps:**
                    1. Update your code to use `firstName` and `lastName`
                    2. If you need full name, concatenate: `${firstName} ${lastName}`
                    
                    ### 2. Orders Endpoint Change
                    
                    **Before (v1):**
                    ```
                    GET /users/{userId}/orders
                    ```
                    
                    **After (v2):**
                    ```
                    GET /orders?customerId={userId}
                    ```
                    
                    ## New Features
                    
                    ### Address Field
                    User responses now include an `address` object.
                    
                    ## Timeline
                    - v1 sunset date: 2025-07-01
                    - v1 deprecation warnings: Enabled
                    """,
                LocalDate.of(2025, 7, 1)
            ));
        }
        
        return ResponseEntity.notFound().build();
    }
}

enum ChangeType {
    ADDED, CHANGED, DEPRECATED, REMOVED, FIXED, SECURITY, BREAKING
}

record Change(ChangeType type, String description) {}
record ChangelogEntry(String version, LocalDate date, List<Change> changes) {}
record Changelog(List<ChangelogEntry> entries) {}
record MigrationGuide(String fromVersion, String toVersion, String content, LocalDate sunsetDate) {}

Anti-Patterns

❌ Outdated Documentation

Documentation that doesn’t match implementation is worse than none.

❌ Missing Error Documentation

# WRONG - only happy path
responses:
  '200':
    description: Success

# ✅ CORRECT - all responses
responses:
  '200':
    description: Success
  '400':
    description: Invalid request
  '401':
    description: Authentication required
  '404':
    description: Resource not found

References