Main Site ↗

api-design-reviewer

by alirezarezvani8.5k621GitHub

API Design Reviewer

Unlock Deep Analysis

Use AI to visualize the workflow and generate a realistic output preview for this skill.

Powered by Fastest LLM

Development
Compatible Agents
Claude Code
Claude Code
~/.claude/skills/
Codex CLI
Codex CLI
~/.codex/skills/
Gemini CLI
Gemini CLI
~/.gemini/skills/
O
OpenCode
~/.opencode/skills/
O
OpenClaw
~/.openclaw/skills/
GitHub Copilot
GitHub Copilot
~/.copilot/skills/
Cursor
Cursor
~/.cursor/skills/
W
Windsurf
~/.codeium/windsurf/skills/
C
Cline
~/.cline/skills/
R
Roo Code
~/.roo/skills/
K
Kiro
~/.kiro/skills/
J
Junie
~/.junie/skills/
A
Augment Code
~/.augment/skills/
W
Warp
~/.warp/skills/
G
Goose
~/.config/goose/skills/
SKILL.md

API Design Reviewer

Tier: POWERFUL
Category: Engineering / Architecture
Maintainer: Claude Skills Team

Overview

The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards.

Core Capabilities

1. API Linting and Convention Analysis

  • Resource Naming Conventions: Enforces kebab-case for resources, camelCase for fields
  • HTTP Method Usage: Validates proper use of GET, POST, PUT, PATCH, DELETE
  • URL Structure: Analyzes endpoint patterns for consistency and RESTful design
  • Status Code Compliance: Ensures appropriate HTTP status codes are used
  • Error Response Formats: Validates consistent error response structures
  • Documentation Coverage: Checks for missing descriptions and documentation gaps

2. Breaking Change Detection

  • Endpoint Removal: Detects removed or deprecated endpoints
  • Response Shape Changes: Identifies modifications to response structures
  • Field Removal: Tracks removed or renamed fields in API responses
  • Type Changes: Catches field type modifications that could break clients
  • Required Field Additions: Flags new required fields that could break existing integrations
  • Status Code Changes: Detects changes to expected status codes

3. API Design Scoring and Assessment

  • Consistency Analysis (30%): Evaluates naming conventions, response patterns, and structural consistency
  • Documentation Quality (20%): Assesses completeness and clarity of API documentation
  • Security Implementation (20%): Reviews authentication, authorization, and security headers
  • Usability Design (15%): Analyzes ease of use, discoverability, and developer experience
  • Performance Patterns (15%): Evaluates caching, pagination, and efficiency patterns

REST Design Principles

Resource Naming Conventions

āœ… Good Examples:
- /api/v1/users
- /api/v1/user-profiles
- /api/v1/orders/123/line-items

āŒ Bad Examples:
- /api/v1/getUsers
- /api/v1/user_profiles
- /api/v1/orders/123/lineItems

HTTP Method Usage

  • GET: Retrieve resources (safe, idempotent)
  • POST: Create new resources (not idempotent)
  • PUT: Replace entire resources (idempotent)
  • PATCH: Partial resource updates (not necessarily idempotent)
  • DELETE: Remove resources (idempotent)

URL Structure Best Practices

Collection Resources: /api/v1/users
Individual Resources: /api/v1/users/123
Nested Resources: /api/v1/users/123/orders
Actions: /api/v1/users/123/activate (POST)
Filtering: /api/v1/users?status=active&role=admin

Versioning Strategies

1. URL Versioning (Recommended)

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

Pros: Clear, explicit, easy to route
Cons: URL proliferation, caching complexity

2. Header Versioning

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

Pros: Clean URLs, content negotiation
Cons: Less visible, harder to test manually

3. Media Type Versioning

GET /api/users
Accept: application/vnd.myapi.v1+json

Pros: RESTful, supports multiple representations
Cons: Complex, harder to implement

4. Query Parameter Versioning

/api/users?version=1

Pros: Simple to implement
Cons: Not RESTful, can be ignored

Pagination Patterns

Offset-Based Pagination

{
  "data": [...],
  "pagination": {
    "offset": 20,
    "limit": 10,
    "total": 150,
    "hasMore": true
  }
}

Cursor-Based Pagination

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

Page-Based Pagination

{
  "data": [...],
  "pagination": {
    "page": 3,
    "pageSize": 10,
    "totalPages": 15,
    "totalItems": 150
  }
}

Error Response Formats

Standard Error Structure

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid parameters",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Email address is not valid"
      }
    ],
    "requestId": "req-123456",
    "timestamp": "2024-02-16T13:00:00Z"
  }
}

HTTP Status Code Usage

  • 400 Bad Request: Invalid request syntax or parameters
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Access denied (authenticated but not authorized)
  • 404 Not Found: Resource not found
  • 409 Conflict: Resource conflict (duplicate, version mismatch)
  • 422 Unprocessable Entity: Valid syntax but semantic errors
  • 429 Too Many Requests: Rate limit exceeded
  • 500 Internal Server Error: Unexpected server error

Authentication and Authorization Patterns

Bearer Token Authentication

Authorization: Bearer <token>

API Key Authentication

X-API-Key: <api-key>
Authorization: Api-Key <api-key>

OAuth 2.0 Flow

Authorization: Bearer <oauth-access-token>

Role-Based Access Control (RBAC)

{
  "user": {
    "id": "123",
    "roles": ["admin", "editor"],
    "permissions": ["read:users", "write:orders"]
  }
}

Rate Limiting Implementation

Headers

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

Response on Limit Exceeded

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests",
    "retryAfter": 3600
  }
}

HATEOAS (Hypermedia as the Engine of Application State)

Example Implementation

{
  "id": "123",
  "name": "John Doe",
  "email": "john@example.com",
  "_links": {
    "self": { "href": "/api/v1/users/123" },
    "orders": { "href": "/api/v1/users/123/orders" },
    "profile": { "href": "/api/v1/users/123/profile" },
    "deactivate": { 
      "href": "/api/v1/users/123/deactivate",
      "method": "POST"
    }
  }
}

Idempotency

Idempotent Methods

  • GET: Always safe and idempotent
  • PUT: Should be idempotent (replace entire resource)
  • DELETE: Should be idempotent (same result)
  • PATCH: May or may not be idempotent

Idempotency Keys

POST /api/v1/payments
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000

Backward Compatibility Guidelines

Safe Changes (Non-Breaking)

  • Adding optional fields to requests
  • Adding fields to responses
  • Adding new endpoints
  • Making required fields optional
  • Adding new enum values (with graceful handling)

Breaking Changes (Require Version Bump)

  • Removing fields from responses
  • Making optional fields required
  • Changing field types
  • Removing endpoints
  • Changing URL structures
  • Modifying error response formats

OpenAPI/Swagger Validation

Required Components

  • API Information: Title, description, version
  • Server Information: Base URLs and descriptions
  • Path Definitions: All endpoints with methods
  • Parameter Definitions: Query, path, header parameters
  • Request/Response Schemas: Complete data models
  • Security Definitions: Authentication schemes
  • Error Responses: Standard error formats

Best Practices

  • Use consistent naming conventions
  • Provide detailed descriptions for all components
  • Include examples for complex objects
  • Define reusable components and schemas
  • Validate against OpenAPI specification

Performance Considerations

Caching Strategies

Cache-Control: public, max-age=3600
ETag: "123456789"
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

Efficient Data Transfer

  • Use appropriate HTTP methods
  • Implement field selection (?fields=id,name,email)
  • Support compression (gzip)
  • Implement efficient pagination
  • Use ETags for conditional requests

Resource Optimization

  • Avoid N+1 queries
  • Implement batch operations
  • Use async processing for heavy operations
  • Support partial updates (PATCH)

Security Best Practices

Input Validation

  • Validate all input parameters
  • Sanitize user data
  • Use parameterized queries
  • Implement request size limits

Authentication Security

  • Use HTTPS everywhere
  • Implement secure token storage
  • Support token expiration and refresh
  • Use strong authentication mechanisms

Authorization Controls

  • Implement principle of least privilege
  • Use resource-based permissions
  • Support fine-grained access control
  • Audit access patterns

Tools and Scripts

api_linter.py

Analyzes API specifications for compliance with REST conventions and best practices.

Features:

  • OpenAPI/Swagger spec validation
  • Naming convention checks
  • HTTP method usage validation
  • Error format consistency
  • Documentation completeness analysis

breaking_change_detector.py

Compares API specification versions to identify breaking changes.

Features:

  • Endpoint comparison
  • Schema change detection
  • Field removal/modification tracking
  • Migration guide generation
  • Impact severity assessment

api_scorecard.py

Provides comprehensive scoring of API design quality.

Features:

  • Multi-dimensional scoring
  • Detailed improvement recommendations
  • Letter grade assessment (A-F)
  • Benchmark comparisons
  • Progress tracking

Integration Examples

CI/CD Integration

- name: "api-linting"
  run: python scripts/api_linter.py openapi.json

- name: "breaking-change-detection"
  run: python scripts/breaking_change_detector.py openapi-v1.json openapi-v2.json

- name: "api-scorecard"
  run: python scripts/api_scorecard.py openapi.json

Pre-commit Hooks

#!/bin/bash
python engineering/api-design-reviewer/scripts/api_linter.py api/openapi.json
if [ $? -ne 0 ]; then
  echo "API linting failed. Please fix the issues before committing."
  exit 1
fi

Best Practices Summary

  1. Consistency First: Maintain consistent naming, response formats, and patterns
  2. Documentation: Provide comprehensive, up-to-date API documentation
  3. Versioning: Plan for evolution with clear versioning strategies
  4. Error Handling: Implement consistent, informative error responses
  5. Security: Build security into every layer of the API
  6. Performance: Design for scale and efficiency from the start
  7. Backward Compatibility: Minimize breaking changes and provide migration paths
  8. Testing: Implement comprehensive testing including contract testing
  9. Monitoring: Add observability for API usage and performance
  10. Developer Experience: Prioritize ease of use and clear documentation

Common Anti-Patterns to Avoid

  1. Verb-based URLs: Use nouns for resources, not actions
  2. Inconsistent Response Formats: Maintain standard response structures
  3. Over-nesting: Avoid deeply nested resource hierarchies
  4. Ignoring HTTP Status Codes: Use appropriate status codes for different scenarios
  5. Poor Error Messages: Provide actionable, specific error information
  6. Missing Pagination: Always paginate list endpoints
  7. No Versioning Strategy: Plan for API evolution from day one
  8. Exposing Internal Structure: Design APIs for external consumption, not internal convenience
  9. Missing Rate Limiting: Protect your API from abuse and overload
  10. Inadequate Testing: Test all aspects including error cases and edge conditions

Conclusion

The API Design Reviewer skill provides a comprehensive framework for building, reviewing, and maintaining high-quality REST APIs. By following these guidelines and using the provided tools, development teams can create APIs that are consistent, well-documented, secure, and maintainable.

Regular use of the linting, breaking change detection, and scoring tools ensures continuous improvement and helps maintain API quality throughout the development lifecycle.


Referenced Files

The following files are referenced in this skill and included for context.

scripts/api_linter.py

#!/usr/bin/env python3
"""
API Linter - Analyzes OpenAPI/Swagger specifications for REST conventions and best practices.

This script validates API designs against established conventions including:
- Resource naming conventions (kebab-case resources, camelCase fields)
- HTTP method usage patterns
- URL structure consistency
- Error response format standards
- Documentation completeness
- Pagination patterns
- Versioning compliance

Supports both OpenAPI JSON specifications and raw endpoint definition JSON.
"""

import argparse
import json
import re
import sys
from typing import Any, Dict, List, Tuple, Optional, Set
from urllib.parse import urlparse
from dataclasses import dataclass, field


@dataclass
class LintIssue:
    """Represents a linting issue found in the API specification."""
    severity: str  # 'error', 'warning', 'info'
    category: str
    message: str
    path: str
    suggestion: str = ""
    line_number: Optional[int] = None


@dataclass
class LintReport:
    """Complete linting report with issues and statistics."""
    issues: List[LintIssue] = field(default_factory=list)
    total_endpoints: int = 0
    endpoints_with_issues: int = 0
    score: float = 0.0
    
    def add_issue(self, issue: LintIssue) -> None:
        """Add an issue to the report."""
        self.issues.append(issue)
    
    def get_issues_by_severity(self) -> Dict[str, List[LintIssue]]:
        """Group issues by severity level."""
        grouped = {'error': [], 'warning': [], 'info': []}
        for issue in self.issues:
            if issue.severity in grouped:
                grouped[issue.severity].append(issue)
        return grouped
    
    def calculate_score(self) -> float:
        """Calculate overall API quality score (0-100)."""
        if self.total_endpoints == 0:
            return 100.0
        
        error_penalty = len([i for i in self.issues if i.severity == 'error']) * 10
        warning_penalty = len([i for i in self.issues if i.severity == 'warning']) * 3
        info_penalty = len([i for i in self.issues if i.severity == 'info']) * 1
        
        total_penalty = error_penalty + warning_penalty + info_penalty
        base_score = 100.0
        
        # Penalty per endpoint to normalize across API sizes
        penalty_per_endpoint = total_penalty / self.total_endpoints if self.total_endpoints > 0 else total_penalty
        
        self.score = max(0.0, base_score - penalty_per_endpoint)
        return self.score


class APILinter:
    """Main API linting engine."""
    
    def __init__(self):
        self.report = LintReport()
        self.openapi_spec: Optional[Dict] = None
        self.raw_endpoints: Optional[Dict] = None
        
        # Regex patterns for naming conventions
        self.kebab_case_pattern = re.compile(r'^[a-z]+(?:-[a-z0-9]+)*$')
        self.camel_case_pattern = re.compile(r'^[a-z][a-zA-Z0-9]*$')
        self.snake_case_pattern = re.compile(r'^[a-z]+(?:_[a-z0-9]+)*$')
        self.pascal_case_pattern = re.compile(r'^[A-Z][a-zA-Z0-9]*$')
        
        # Standard HTTP methods
        self.http_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}
        
        # Standard HTTP status codes by method
        self.standard_status_codes = {
            'GET': {200, 304, 404},
            'POST': {200, 201, 400, 409, 422},
            'PUT': {200, 204, 400, 404, 409},
            'PATCH': {200, 204, 400, 404, 409},
            'DELETE': {200, 204, 404},
            'HEAD': {200, 404},
            'OPTIONS': {200}
        }
        
        # Common error status codes
        self.common_error_codes = {400, 401, 403, 404, 405, 409, 422, 429, 500, 502, 503}

    def lint_openapi_spec(self, spec: Dict[str, Any]) -> LintReport:
        """Lint an OpenAPI/Swagger specification."""
        self.openapi_spec = spec
        self.report = LintReport()
        
        # Basic structure validation
        self._validate_openapi_structure()
        
        # Info section validation
        self._validate_info_section()
        
        # Server section validation
        self._validate_servers_section()
        
        # Paths validation (main linting logic)
        self._validate_paths_section()
        
        # Components validation
        self._validate_components_section()
        
        # Security validation
        self._validate_security_section()
        
        # Calculate final score
        self.report.calculate_score()
        
        return self.report

    def lint_raw_endpoints(self, endpoints: Dict[str, Any]) -> LintReport:
        """Lint raw endpoint definitions."""
        self.raw_endpoints = endpoints
        self.report = LintReport()
        
        # Validate raw endpoint structure
        self._validate_raw_endpoint_structure()
        
        # Lint each endpoint
        for endpoint_path, endpoint_data in endpoints.get('endpoints', {}).items():
            self._lint_raw_endpoint(endpoint_path, endpoint_data)
        
        self.report.calculate_score()
        return self.report

    def _validate_openapi_structure(self) -> None:
        """Validate basic OpenAPI document structure."""
        required_fields = ['openapi', 'info', 'paths']
        
        for field in required_fields:
            if field not in self.openapi_spec:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='structure',
                    message=f"Missing required field: {field}",
                    path=f"/{field}",
                    suggestion=f"Add the '{field}' field to the root of your OpenAPI specification"
                ))

    def _validate_info_section(self) -> None:
        """Validate the info section of OpenAPI spec."""
        if 'info' not in self.openapi_spec:
            return
            
        info = self.openapi_spec['info']
        required_info_fields = ['title', 'version']
        recommended_info_fields = ['description', 'contact']
        
        for field in required_info_fields:
            if field not in info:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='documentation',
                    message=f"Missing required info field: {field}",
                    path=f"/info/{field}",
                    suggestion=f"Add a '{field}' field to the info section"
                ))
        
        for field in recommended_info_fields:
            if field not in info:
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='documentation',
                    message=f"Missing recommended info field: {field}",
                    path=f"/info/{field}",
                    suggestion=f"Consider adding a '{field}' field to improve API documentation"
                ))
        
        # Validate version format
        if 'version' in info:
            version = info['version']
            if not re.match(r'^\d+\.\d+(\.\d+)?(-\w+)?$', version):
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='versioning',
                    message=f"Version format '{version}' doesn't follow semantic versioning",
                    path="/info/version",
                    suggestion="Use semantic versioning format (e.g., '1.0.0', '2.1.3-beta')"
                ))

    def _validate_servers_section(self) -> None:
        """Validate the servers section."""
        if 'servers' not in self.openapi_spec:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='configuration',
                message="Missing servers section",
                path="/servers",
                suggestion="Add a servers section to specify API base URLs"
            ))
            return
        
        servers = self.openapi_spec['servers']
        if not isinstance(servers, list) or len(servers) == 0:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='configuration',
                message="Empty servers section",
                path="/servers",
                suggestion="Add at least one server URL"
            ))

    def _validate_paths_section(self) -> None:
        """Validate all API paths and operations."""
        if 'paths' not in self.openapi_spec:
            return
            
        paths = self.openapi_spec['paths']
        if not paths:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message="No paths defined in API specification",
                path="/paths",
                suggestion="Define at least one API endpoint"
            ))
            return
        
        self.report.total_endpoints = sum(
            len([method for method in path_obj.keys() if method.upper() in self.http_methods])
            for path_obj in paths.values() if isinstance(path_obj, dict)
        )
        
        endpoints_with_issues = set()
        
        for path, path_obj in paths.items():
            if not isinstance(path_obj, dict):
                continue
                
            # Validate path structure
            path_issues = self._validate_path_structure(path)
            if path_issues:
                endpoints_with_issues.add(path)
            
            # Validate each operation in the path
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                    
                operation_issues = self._validate_operation(path, method.upper(), operation)
                if operation_issues:
                    endpoints_with_issues.add(path)
        
        self.report.endpoints_with_issues = len(endpoints_with_issues)

    def _validate_path_structure(self, path: str) -> bool:
        """Validate REST path structure and naming conventions."""
        has_issues = False
        
        # Check if path starts with slash
        if not path.startswith('/'):
            self.report.add_issue(LintIssue(
                severity='error',
                category='url_structure',
                message=f"Path must start with '/' character: {path}",
                path=f"/paths/{path}",
                suggestion=f"Change '{path}' to '/{path.lstrip('/')}'"
            ))
            has_issues = True
        
        # Split path into segments
        segments = [seg for seg in path.split('/') if seg]
        
        # Check for empty segments (double slashes)
        if '//' in path:
            self.report.add_issue(LintIssue(
                severity='error',
                category='url_structure',
                message=f"Path contains empty segments: {path}",
                path=f"/paths/{path}",
                suggestion="Remove double slashes from the path"
            ))
            has_issues = True
        
        # Validate each segment
        for i, segment in enumerate(segments):
            # Skip parameter segments
            if segment.startswith('{') and segment.endswith('}'):
                # Validate parameter naming
                param_name = segment[1:-1]
                if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Path parameter '{param_name}' should use camelCase or kebab-case",
                        path=f"/paths/{path}",
                        suggestion=f"Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')"
                    ))
                    has_issues = True
                continue
            
            # Check for resource naming conventions
            if not self.kebab_case_pattern.match(segment):
                # Allow version segments like 'v1', 'v2'
                if not re.match(r'^v\d+$', segment):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Resource segment '{segment}' should use kebab-case",
                        path=f"/paths/{path}",
                        suggestion=f"Use kebab-case for '{segment}' (e.g., 'user-profiles', 'order-items')"
                    ))
                    has_issues = True
            
            # Check for verb usage in URLs (anti-pattern)
            common_verbs = {'get', 'post', 'put', 'delete', 'create', 'update', 'remove', 'add'}
            if segment.lower() in common_verbs:
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='rest_conventions',
                    message=f"Avoid verbs in URLs: '{segment}' in {path}",
                    path=f"/paths/{path}",
                    suggestion="Use HTTP methods instead of verbs in URLs. Use nouns for resources."
                ))
                has_issues = True
        
        # Check path depth (avoid over-nesting)
        if len(segments) > 6:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='url_structure',
                message=f"Path has excessive nesting ({len(segments)} levels): {path}",
                path=f"/paths/{path}",
                suggestion="Consider flattening the resource hierarchy or using query parameters"
            ))
            has_issues = True
        
        # Check for consistent versioning
        if any('v' + str(i) in segments for i in range(1, 10)):
            version_segments = [seg for seg in segments if re.match(r'^v\d+$', seg)]
            if len(version_segments) > 1:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='versioning',
                    message=f"Multiple version segments in path: {path}",
                    path=f"/paths/{path}",
                    suggestion="Use only one version segment per path"
                ))
                has_issues = True
        
        return has_issues

    def _validate_operation(self, path: str, method: str, operation: Dict[str, Any]) -> bool:
        """Validate individual operation (HTTP method + path combination)."""
        has_issues = False
        operation_path = f"/paths/{path}/{method.lower()}"
        
        # Check for required operation fields
        if 'responses' not in operation:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message=f"Missing responses section for {method} {path}",
                path=f"{operation_path}/responses",
                suggestion="Define expected responses for this operation"
            ))
            has_issues = True
        
        # Check for operation documentation
        if 'summary' not in operation:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='documentation',
                message=f"Missing summary for {method} {path}",
                path=f"{operation_path}/summary",
                suggestion="Add a brief summary describing what this operation does"
            ))
            has_issues = True
        
        if 'description' not in operation:
            self.report.add_issue(LintIssue(
                severity='info',
                category='documentation',
                message=f"Missing description for {method} {path}",
                path=f"{operation_path}/description",
                suggestion="Add a detailed description for better API documentation"
            ))
            has_issues = True
        
        # Validate HTTP method usage patterns
        method_issues = self._validate_http_method_usage(path, method, operation)
        if method_issues:
            has_issues = True
        
        # Validate responses
        if 'responses' in operation:
            response_issues = self._validate_responses(path, method, operation['responses'])
            if response_issues:
                has_issues = True
        
        # Validate parameters
        if 'parameters' in operation:
            param_issues = self._validate_parameters(path, method, operation['parameters'])
            if param_issues:
                has_issues = True
        
        # Validate request body
        if 'requestBody' in operation:
            body_issues = self._validate_request_body(path, method, operation['requestBody'])
            if body_issues:
                has_issues = True
        
        return has_issues

    def _validate_http_method_usage(self, path: str, method: str, operation: Dict[str, Any]) -> bool:
        """Validate proper HTTP method usage patterns."""
        has_issues = False
        
        # GET requests should not have request body
        if method == 'GET' and 'requestBody' in operation:
            self.report.add_issue(LintIssue(
                severity='error',
                category='rest_conventions',
                message=f"GET request should not have request body: {method} {path}",
                path=f"/paths/{path}/{method.lower()}/requestBody",
                suggestion="Remove requestBody from GET request or use POST if body is needed"
            ))
            has_issues = True
        
        # DELETE requests typically should not have request body
        if method == 'DELETE' and 'requestBody' in operation:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='rest_conventions',
                message=f"DELETE request typically should not have request body: {method} {path}",
                path=f"/paths/{path}/{method.lower()}/requestBody",
                suggestion="Consider using query parameters or path parameters instead"
            ))
            has_issues = True
        
        # POST/PUT/PATCH should typically have request body (except for actions)
        if method in ['POST', 'PUT', 'PATCH'] and 'requestBody' not in operation:
            # Check if this is an action endpoint
            if not any(action in path.lower() for action in ['activate', 'deactivate', 'reset', 'confirm']):
                self.report.add_issue(LintIssue(
                    severity='info',
                    category='rest_conventions',
                    message=f"{method} request typically should have request body: {method} {path}",
                    path=f"/paths/{path}/{method.lower()}",
                    suggestion=f"Consider adding requestBody for {method} operation or use GET if no data is being sent"
                ))
                has_issues = True
        
        return has_issues

    def _validate_responses(self, path: str, method: str, responses: Dict[str, Any]) -> bool:
        """Validate response definitions."""
        has_issues = False
        
        # Check for success response
        success_codes = {'200', '201', '202', '204'}
        has_success = any(code in responses for code in success_codes)
        
        if not has_success:
            self.report.add_issue(LintIssue(
                severity='error',
                category='responses',
                message=f"Missing success response for {method} {path}",
                path=f"/paths/{path}/{method.lower()}/responses",
                suggestion="Define at least one success response (200, 201, 202, or 204)"
            ))
            has_issues = True
        
        # Check for error responses
        has_error_responses = any(code.startswith('4') or code.startswith('5') for code in responses.keys())
        
        if not has_error_responses:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='responses',
                message=f"Missing error responses for {method} {path}",
                path=f"/paths/{path}/{method.lower()}/responses",
                suggestion="Define common error responses (400, 404, 500, etc.)"
            ))
            has_issues = True
        
        # Validate individual response codes
        for status_code, response in responses.items():
            if status_code == 'default':
                continue
                
            try:
                code_int = int(status_code)
            except ValueError:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='responses',
                    message=f"Invalid status code '{status_code}' for {method} {path}",
                    path=f"/paths/{path}/{method.lower()}/responses/{status_code}",
                    suggestion="Use valid HTTP status codes (e.g., 200, 404, 500)"
                ))
                has_issues = True
                continue
            
            # Check if status code is appropriate for the method
            expected_codes = self.standard_status_codes.get(method, set())
            common_codes = {400, 401, 403, 404, 429, 500}  # Always acceptable
            
            if expected_codes and code_int not in expected_codes and code_int not in common_codes:
                self.report.add_issue(LintIssue(
                    severity='info',
                    category='responses',
                    message=f"Uncommon status code {status_code} for {method} {path}",
                    path=f"/paths/{path}/{method.lower()}/responses/{status_code}",
                    suggestion=f"Consider using standard codes for {method}: {sorted(expected_codes)}"
                ))
                has_issues = True
        
        return has_issues

    def _validate_parameters(self, path: str, method: str, parameters: List[Dict[str, Any]]) -> bool:
        """Validate parameter definitions."""
        has_issues = False
        
        for i, param in enumerate(parameters):
            param_path = f"/paths/{path}/{method.lower()}/parameters[{i}]"
            
            # Check required fields
            if 'name' not in param:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='parameters',
                    message=f"Parameter missing name field in {method} {path}",
                    path=f"{param_path}/name",
                    suggestion="Add a name field to the parameter"
                ))
                has_issues = True
                continue
            
            if 'in' not in param:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='parameters',
                    message=f"Parameter '{param['name']}' missing 'in' field in {method} {path}",
                    path=f"{param_path}/in",
                    suggestion="Specify parameter location (query, path, header, cookie)"
                ))
                has_issues = True
            
            # Validate parameter naming
            param_name = param['name']
            param_location = param.get('in', '')
            
            if param_location == 'query':
                # Query parameters should use camelCase or kebab-case
                if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Query parameter '{param_name}' should use camelCase or kebab-case in {method} {path}",
                        path=f"{param_path}/name",
                        suggestion="Use camelCase (e.g., 'pageSize') or kebab-case (e.g., 'page-size')"
                    ))
                    has_issues = True
            
            elif param_location == 'path':
                # Path parameters should use camelCase or kebab-case
                if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Path parameter '{param_name}' should use camelCase or kebab-case in {method} {path}",
                        path=f"{param_path}/name",
                        suggestion="Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')"
                    ))
                    has_issues = True
                
                # Path parameters must be required
                if not param.get('required', False):
                    self.report.add_issue(LintIssue(
                        severity='error',
                        category='parameters',
                        message=f"Path parameter '{param_name}' must be required in {method} {path}",
                        path=f"{param_path}/required",
                        suggestion="Set required: true for path parameters"
                    ))
                    has_issues = True
        
        return has_issues

    def _validate_request_body(self, path: str, method: str, request_body: Dict[str, Any]) -> bool:
        """Validate request body definition."""
        has_issues = False
        
        if 'content' not in request_body:
            self.report.add_issue(LintIssue(
                severity='error',
                category='request_body',
                message=f"Request body missing content for {method} {path}",
                path=f"/paths/{path}/{method.lower()}/requestBody/content",
                suggestion="Define content types for the request body"
            ))
            has_issues = True
        
        return has_issues

    def _validate_components_section(self) -> None:
        """Validate the components section."""
        if 'components' not in self.openapi_spec:
            self.report.add_issue(LintIssue(
                severity='info',
                category='structure',
                message="Missing components section",
                path="/components",
                suggestion="Consider defining reusable components (schemas, responses, parameters)"
            ))
            return
        
        components = self.openapi_spec['components']
        
        # Validate schemas
        if 'schemas' in components:
            self._validate_schemas(components['schemas'])

    def _validate_schemas(self, schemas: Dict[str, Any]) -> None:
        """Validate schema definitions."""
        for schema_name, schema in schemas.items():
            # Check schema naming (should be PascalCase)
            if not self.pascal_case_pattern.match(schema_name):
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='naming',
                    message=f"Schema name '{schema_name}' should use PascalCase",
                    path=f"/components/schemas/{schema_name}",
                    suggestion=f"Use PascalCase for schema names (e.g., 'UserProfile', 'OrderItem')"
                ))
            
            # Validate schema properties
            if isinstance(schema, dict) and 'properties' in schema:
                self._validate_schema_properties(schema_name, schema['properties'])

    def _validate_schema_properties(self, schema_name: str, properties: Dict[str, Any]) -> None:
        """Validate schema property naming."""
        for prop_name, prop_def in properties.items():
            # Properties should use camelCase
            if not self.camel_case_pattern.match(prop_name):
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='naming',
                    message=f"Property '{prop_name}' in schema '{schema_name}' should use camelCase",
                    path=f"/components/schemas/{schema_name}/properties/{prop_name}",
                    suggestion="Use camelCase for property names (e.g., 'firstName', 'createdAt')"
                ))

    def _validate_security_section(self) -> None:
        """Validate security definitions."""
        if 'security' not in self.openapi_spec and 'components' not in self.openapi_spec:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='security',
                message="No security configuration found",
                path="/security",
                suggestion="Define security schemes and apply them to operations"
            ))

    def _validate_raw_endpoint_structure(self) -> None:
        """Validate structure of raw endpoint definitions."""
        if 'endpoints' not in self.raw_endpoints:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message="Missing 'endpoints' field in raw endpoint definition",
                path="/endpoints",
                suggestion="Provide an 'endpoints' object containing endpoint definitions"
            ))
            return
        
        endpoints = self.raw_endpoints['endpoints']
        self.report.total_endpoints = len(endpoints)

    def _lint_raw_endpoint(self, path: str, endpoint_data: Dict[str, Any]) -> None:
        """Lint individual raw endpoint definition."""
        # Validate path structure
        self._validate_path_structure(path)
        
        # Check for required fields
        if 'method' not in endpoint_data:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message=f"Missing method field for endpoint {path}",
                path=f"/endpoints/{path}/method",
                suggestion="Specify HTTP method (GET, POST, PUT, PATCH, DELETE)"
            ))
            return
        
        method = endpoint_data['method'].upper()
        if method not in self.http_methods:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message=f"Invalid HTTP method '{method}' for endpoint {path}",
                path=f"/endpoints/{path}/method",
                suggestion=f"Use valid HTTP methods: {', '.join(sorted(self.http_methods))}"
            ))

    def generate_json_report(self) -> str:
        """Generate JSON format report."""
        issues_by_severity = self.report.get_issues_by_severity()
        
        report_data = {
            "summary": {
                "total_endpoints": self.report.total_endpoints,
                "endpoints_with_issues": self.report.endpoints_with_issues,
                "total_issues": len(self.report.issues),
                "errors": len(issues_by_severity['error']),
                "warnings": len(issues_by_severity['warning']),
                "info": len(issues_by_severity['info']),
                "score": round(self.report.score, 2)
            },
            "issues": []
        }
        
        for issue in self.report.issues:
            report_data["issues"].append({
                "severity": issue.severity,
                "category": issue.category,
                "message": issue.message,
                "path": issue.path,
                "suggestion": issue.suggestion
            })
        
        return json.dumps(report_data, indent=2)

    def generate_text_report(self) -> str:
        """Generate human-readable text report."""
        issues_by_severity = self.report.get_issues_by_severity()
        
        report_lines = [
            "═══════════════════════════════════════════════════════════════",
            "                      API LINTING REPORT",
            "═══════════════════════════════════════════════════════════════",
            "",
            "SUMMARY:",
            f"  Total Endpoints: {self.report.total_endpoints}",
            f"  Endpoints with Issues: {self.report.endpoints_with_issues}",
            f"  Overall Score: {self.report.score:.1f}/100.0",
            "",
            "ISSUE BREAKDOWN:",
            f"  šŸ”“ Errors: {len(issues_by_severity['error'])}",
            f"  🟔 Warnings: {len(issues_by_severity['warning'])}",
            f"  ā„¹ļø  Info: {len(issues_by_severity['info'])}",
            "",
        ]
        
        if not self.report.issues:
            report_lines.extend([
                "šŸŽ‰ Congratulations! No issues found in your API specification.",
                ""
            ])
        else:
            # Group issues by category
            issues_by_category = {}
            for issue in self.report.issues:
                if issue.category not in issues_by_category:
                    issues_by_category[issue.category] = []
                issues_by_category[issue.category].append(issue)
            
            for category, issues in issues_by_category.items():
                report_lines.append(f"{'═' * 60}")
                report_lines.append(f"CATEGORY: {category.upper().replace('_', ' ')}")
                report_lines.append(f"{'═' * 60}")
                
                for issue in issues:
                    severity_icon = {"error": "šŸ”“", "warning": "🟔", "info": "ā„¹ļø"}[issue.severity]
                    
                    report_lines.extend([
                        f"{severity_icon} {issue.severity.upper()}: {issue.message}",
                        f"   Path: {issue.path}",
                    ])
                    
                    if issue.suggestion:
                        report_lines.append(f"   šŸ’” Suggestion: {issue.suggestion}")
                    
                    report_lines.append("")
        
        # Add scoring breakdown
        report_lines.extend([
            "═══════════════════════════════════════════════════════════════",
            "SCORING DETAILS:",
            "═══════════════════════════════════════════════════════════════",
            f"Base Score: 100.0",
            f"Errors Penalty: -{len(issues_by_severity['error']) * 10} (10 points per error)",
            f"Warnings Penalty: -{len(issues_by_severity['warning']) * 3} (3 points per warning)",
            f"Info Penalty: -{len(issues_by_severity['info']) * 1} (1 point per info)",
            f"Final Score: {self.report.score:.1f}/100.0",
            ""
        ])
        
        # Add recommendations based on score
        if self.report.score >= 90:
            report_lines.append("šŸ† Excellent! Your API design follows best practices.")
        elif self.report.score >= 80:
            report_lines.append("āœ… Good API design with minor areas for improvement.")
        elif self.report.score >= 70:
            report_lines.append("āš ļø  Fair API design. Consider addressing warnings and errors.")
        elif self.report.score >= 50:
            report_lines.append("āŒ Poor API design. Multiple issues need attention.")
        else:
            report_lines.append("🚨 Critical API design issues. Immediate attention required.")
        
        return "\n".join(report_lines)


def main():
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        description="Analyze OpenAPI/Swagger specifications for REST conventions and best practices",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python api_linter.py openapi.json
  python api_linter.py --format json openapi.json > report.json
  python api_linter.py --raw-endpoints endpoints.json
        """
    )
    
    parser.add_argument(
        'input_file',
        help='Input file: OpenAPI/Swagger JSON file or raw endpoints JSON'
    )
    
    parser.add_argument(
        '--format',
        choices=['text', 'json'],
        default='text',
        help='Output format (default: text)'
    )
    
    parser.add_argument(
        '--raw-endpoints',
        action='store_true',
        help='Treat input as raw endpoint definitions instead of OpenAPI spec'
    )
    
    parser.add_argument(
        '--output',
        help='Output file (default: stdout)'
    )
    
    args = parser.parse_args()
    
    # Load input file
    try:
        with open(args.input_file, 'r') as f:
            input_data = json.load(f)
    except FileNotFoundError:
        print(f"Error: Input file '{args.input_file}' not found.", file=sys.stderr)
        return 1
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in '{args.input_file}': {e}", file=sys.stderr)
        return 1
    
    # Initialize linter and run analysis
    linter = APILinter()
    
    try:
        if args.raw_endpoints:
            report = linter.lint_raw_endpoints(input_data)
        else:
            report = linter.lint_openapi_spec(input_data)
    except Exception as e:
        print(f"Error during linting: {e}", file=sys.stderr)
        return 1
    
    # Generate report
    if args.format == 'json':
        output = linter.generate_json_report()
    else:
        output = linter.generate_text_report()
    
    # Write output
    if args.output:
        try:
            with open(args.output, 'w') as f:
                f.write(output)
            print(f"Report written to {args.output}")
        except IOError as e:
            print(f"Error writing to '{args.output}': {e}", file=sys.stderr)
            return 1
    else:
        print(output)
    
    # Return appropriate exit code
    error_count = len([i for i in report.issues if i.severity == 'error'])
    return 1 if error_count > 0 else 0


if __name__ == '__main__':
    sys.exit(main())

scripts/breaking_change_detector.py

#!/usr/bin/env python3
"""
Breaking Change Detector - Compares API specification versions to identify breaking changes.

This script analyzes two versions of an API specification and detects potentially
breaking changes including:
- Removed endpoints
- Modified response structures
- Removed or renamed fields
- Field type changes
- New required fields
- HTTP status code changes
- Parameter changes

Generates detailed reports with migration guides for each breaking change.
"""

import argparse
import json
import sys
from typing import Any, Dict, List, Set, Optional, Tuple, Union
from dataclasses import dataclass, field
from enum import Enum


class ChangeType(Enum):
    """Types of API changes."""
    BREAKING = "breaking"
    POTENTIALLY_BREAKING = "potentially_breaking"
    NON_BREAKING = "non_breaking"
    ENHANCEMENT = "enhancement"


class ChangeSeverity(Enum):
    """Severity levels for changes."""
    CRITICAL = "critical"     # Will definitely break clients
    HIGH = "high"            # Likely to break some clients
    MEDIUM = "medium"        # May break clients depending on usage
    LOW = "low"             # Minor impact, unlikely to break clients
    INFO = "info"           # Informational, no breaking impact


@dataclass
class Change:
    """Represents a detected change between API versions."""
    change_type: ChangeType
    severity: ChangeSeverity
    category: str
    path: str
    message: str
    old_value: Any = None
    new_value: Any = None
    migration_guide: str = ""
    impact_description: str = ""
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert change to dictionary for JSON serialization."""
        return {
            "changeType": self.change_type.value,
            "severity": self.severity.value,
            "category": self.category,
            "path": self.path,
            "message": self.message,
            "oldValue": self.old_value,
            "newValue": self.new_value,
            "migrationGuide": self.migration_guide,
            "impactDescription": self.impact_description
        }


@dataclass
class ComparisonReport:
    """Complete comparison report between two API versions."""
    changes: List[Change] = field(default_factory=list)
    summary: Dict[str, int] = field(default_factory=dict)
    
    def add_change(self, change: Change) -> None:
        """Add a change to the report."""
        self.changes.append(change)
    
    def calculate_summary(self) -> None:
        """Calculate summary statistics."""
        self.summary = {
            "total_changes": len(self.changes),
            "breaking_changes": len([c for c in self.changes if c.change_type == ChangeType.BREAKING]),
            "potentially_breaking_changes": len([c for c in self.changes if c.change_type == ChangeType.POTENTIALLY_BREAKING]),
            "non_breaking_changes": len([c for c in self.changes if c.change_type == ChangeType.NON_BREAKING]),
            "enhancements": len([c for c in self.changes if c.change_type == ChangeType.ENHANCEMENT]),
            "critical_severity": len([c for c in self.changes if c.severity == ChangeSeverity.CRITICAL]),
            "high_severity": len([c for c in self.changes if c.severity == ChangeSeverity.HIGH]),
            "medium_severity": len([c for c in self.changes if c.severity == ChangeSeverity.MEDIUM]),
            "low_severity": len([c for c in self.changes if c.severity == ChangeSeverity.LOW]),
            "info_severity": len([c for c in self.changes if c.severity == ChangeSeverity.INFO])
        }
    
    def has_breaking_changes(self) -> bool:
        """Check if report contains any breaking changes."""
        return any(c.change_type in [ChangeType.BREAKING, ChangeType.POTENTIALLY_BREAKING] 
                  for c in self.changes)


class BreakingChangeDetector:
    """Main breaking change detection engine."""
    
    def __init__(self):
        self.report = ComparisonReport()
        self.old_spec: Optional[Dict] = None
        self.new_spec: Optional[Dict] = None
    
    def compare_specs(self, old_spec: Dict[str, Any], new_spec: Dict[str, Any]) -> ComparisonReport:
        """Compare two API specifications and detect changes."""
        self.old_spec = old_spec
        self.new_spec = new_spec
        self.report = ComparisonReport()
        
        # Compare different sections of the API specification
        self._compare_info_section()
        self._compare_servers_section()
        self._compare_paths_section()
        self._compare_components_section()
        self._compare_security_section()
        
        # Calculate summary statistics
        self.report.calculate_summary()
        
        return self.report
    
    def _compare_info_section(self) -> None:
        """Compare API info sections."""
        old_info = self.old_spec.get('info', {})
        new_info = self.new_spec.get('info', {})
        
        # Version comparison
        old_version = old_info.get('version', '')
        new_version = new_info.get('version', '')
        
        if old_version != new_version:
            self.report.add_change(Change(
                change_type=ChangeType.NON_BREAKING,
                severity=ChangeSeverity.INFO,
                category="versioning",
                path="/info/version",
                message=f"API version changed from '{old_version}' to '{new_version}'",
                old_value=old_version,
                new_value=new_version,
                impact_description="Version change indicates API evolution"
            ))
        
        # Title comparison
        old_title = old_info.get('title', '')
        new_title = new_info.get('title', '')
        
        if old_title != new_title:
            self.report.add_change(Change(
                change_type=ChangeType.NON_BREAKING,
                severity=ChangeSeverity.INFO,
                category="metadata",
                path="/info/title",
                message=f"API title changed from '{old_title}' to '{new_title}'",
                old_value=old_title,
                new_value=new_title,
                impact_description="Title change is cosmetic and doesn't affect functionality"
            ))
    
    def _compare_servers_section(self) -> None:
        """Compare server configurations."""
        old_servers = self.old_spec.get('servers', [])
        new_servers = self.new_spec.get('servers', [])
        
        old_urls = {server.get('url', '') for server in old_servers if isinstance(server, dict)}
        new_urls = {server.get('url', '') for server in new_servers if isinstance(server, dict)}
        
        # Removed servers
        removed_urls = old_urls - new_urls
        for url in removed_urls:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.HIGH,
                category="servers",
                path="/servers",
                message=f"Server URL removed: {url}",
                old_value=url,
                new_value=None,
                migration_guide=f"Update client configurations to use alternative server URLs: {list(new_urls)}",
                impact_description="Clients configured to use removed server URL will fail to connect"
            ))
        
        # Added servers
        added_urls = new_urls - old_urls
        for url in added_urls:
            self.report.add_change(Change(
                change_type=ChangeType.ENHANCEMENT,
                severity=ChangeSeverity.INFO,
                category="servers",
                path="/servers",
                message=f"New server URL added: {url}",
                old_value=None,
                new_value=url,
                impact_description="New server option provides additional deployment flexibility"
            ))
    
    def _compare_paths_section(self) -> None:
        """Compare API paths and operations."""
        old_paths = self.old_spec.get('paths', {})
        new_paths = self.new_spec.get('paths', {})
        
        # Find removed, added, and modified paths
        old_path_set = set(old_paths.keys())
        new_path_set = set(new_paths.keys())
        
        removed_paths = old_path_set - new_path_set
        added_paths = new_path_set - old_path_set
        common_paths = old_path_set & new_path_set
        
        # Handle removed paths
        for path in removed_paths:
            old_operations = self._extract_operations(old_paths[path])
            for method in old_operations:
                self.report.add_change(Change(
                    change_type=ChangeType.BREAKING,
                    severity=ChangeSeverity.CRITICAL,
                    category="endpoints",
                    path=f"/paths{path}",
                    message=f"Endpoint removed: {method.upper()} {path}",
                    old_value=f"{method.upper()} {path}",
                    new_value=None,
                    migration_guide=self._generate_endpoint_removal_migration(path, method, new_paths),
                    impact_description="Clients using this endpoint will receive 404 errors"
                ))
        
        # Handle added paths
        for path in added_paths:
            new_operations = self._extract_operations(new_paths[path])
            for method in new_operations:
                self.report.add_change(Change(
                    change_type=ChangeType.ENHANCEMENT,
                    severity=ChangeSeverity.INFO,
                    category="endpoints",
                    path=f"/paths{path}",
                    message=f"New endpoint added: {method.upper()} {path}",
                    old_value=None,
                    new_value=f"{method.upper()} {path}",
                    impact_description="New functionality available to clients"
                ))
        
        # Handle modified paths
        for path in common_paths:
            self._compare_path_operations(path, old_paths[path], new_paths[path])
    
    def _extract_operations(self, path_object: Dict[str, Any]) -> List[str]:
        """Extract HTTP operations from a path object."""
        http_methods = {'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'}
        return [method for method in path_object.keys() if method.lower() in http_methods]
    
    def _compare_path_operations(self, path: str, old_path_obj: Dict, new_path_obj: Dict) -> None:
        """Compare operations within a specific path."""
        old_operations = set(self._extract_operations(old_path_obj))
        new_operations = set(self._extract_operations(new_path_obj))
        
        # Removed operations
        removed_ops = old_operations - new_operations
        for method in removed_ops:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.CRITICAL,
                category="endpoints",
                path=f"/paths{path}/{method}",
                message=f"HTTP method removed: {method.upper()} {path}",
                old_value=f"{method.upper()} {path}",
                new_value=None,
                migration_guide=self._generate_method_removal_migration(path, method, new_operations),
                impact_description="Clients using this method will receive 405 Method Not Allowed errors"
            ))
        
        # Added operations
        added_ops = new_operations - old_operations
        for method in added_ops:
            self.report.add_change(Change(
                change_type=ChangeType.ENHANCEMENT,
                severity=ChangeSeverity.INFO,
                category="endpoints",
                path=f"/paths{path}/{method}",
                message=f"New HTTP method added: {method.upper()} {path}",
                old_value=None,
                new_value=f"{method.upper()} {path}",
                impact_description="New method provides additional functionality for this resource"
            ))
        
        # Modified operations
        common_ops = old_operations & new_operations
        for method in common_ops:
            self._compare_operation_details(path, method, old_path_obj[method], new_path_obj[method])
    
    def _compare_operation_details(self, path: str, method: str, old_op: Dict, new_op: Dict) -> None:
        """Compare details of individual operations."""
        operation_path = f"/paths{path}/{method}"
        
        # Compare parameters
        self._compare_parameters(operation_path, old_op.get('parameters', []), new_op.get('parameters', []))
        
        # Compare request body
        self._compare_request_body(operation_path, old_op.get('requestBody'), new_op.get('requestBody'))
        
        # Compare responses
        self._compare_responses(operation_path, old_op.get('responses', {}), new_op.get('responses', {}))
        
        # Compare security requirements
        self._compare_security_requirements(operation_path, old_op.get('security'), new_op.get('security'))
    
    def _compare_parameters(self, base_path: str, old_params: List[Dict], new_params: List[Dict]) -> None:
        """Compare operation parameters."""
        # Create lookup dictionaries
        old_param_map = {(p.get('name'), p.get('in')): p for p in old_params}
        new_param_map = {(p.get('name'), p.get('in')): p for p in new_params}
        
        old_param_keys = set(old_param_map.keys())
        new_param_keys = set(new_param_map.keys())
        
        # Removed parameters
        removed_params = old_param_keys - new_param_keys
        for param_key in removed_params:
            name, location = param_key
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.HIGH,
                category="parameters",
                path=f"{base_path}/parameters",
                message=f"Parameter removed: {name} (in: {location})",
                old_value=old_param_map[param_key],
                new_value=None,
                migration_guide=f"Remove '{name}' parameter from {location} when calling this endpoint",
                impact_description="Clients sending this parameter may receive validation errors"
            ))
        
        # Added parameters
        added_params = new_param_keys - old_param_keys
        for param_key in added_params:
            name, location = param_key
            new_param = new_param_map[param_key]
            is_required = new_param.get('required', False)
            
            if is_required:
                self.report.add_change(Change(
                    change_type=ChangeType.BREAKING,
                    severity=ChangeSeverity.CRITICAL,
                    category="parameters",
                    path=f"{base_path}/parameters",
                    message=f"New required parameter added: {name} (in: {location})",
                    old_value=None,
                    new_value=new_param,
                    migration_guide=f"Add required '{name}' parameter to {location} when calling this endpoint",
                    impact_description="Clients not providing this parameter will receive 400 Bad Request errors"
                ))
            else:
                self.report.add_change(Change(
                    change_type=ChangeType.NON_BREAKING,
                    severity=ChangeSeverity.INFO,
                    category="parameters",
                    path=f"{base_path}/parameters",
                    message=f"New optional parameter added: {name} (in: {location})",
                    old_value=None,
                    new_value=new_param,
                    impact_description="Optional parameter provides additional functionality"
                ))
        
        # Modified parameters
        common_params = old_param_keys & new_param_keys
        for param_key in common_params:
            name, location = param_key
            old_param = old_param_map[param_key]
            new_param = new_param_map[param_key]
            self._compare_parameter_details(base_path, name, location, old_param, new_param)
    
    def _compare_parameter_details(self, base_path: str, name: str, location: str, 
                                 old_param: Dict, new_param: Dict) -> None:
        """Compare individual parameter details."""
        param_path = f"{base_path}/parameters/{name}"
        
        # Required status change
        old_required = old_param.get('required', False)
        new_required = new_param.get('required', False)
        
        if old_required != new_required:
            if new_required:
                self.report.add_change(Change(
                    change_type=ChangeType.BREAKING,
                    severity=ChangeSeverity.HIGH,
                    category="parameters",
                    path=param_path,
                    message=f"Parameter '{name}' is now required (was optional)",
                    old_value=old_required,
                    new_value=new_required,
                    migration_guide=f"Ensure '{name}' parameter is always provided when calling this endpoint",
                    impact_description="Clients not providing this parameter will receive validation errors"
                ))
            else:
                self.report.add_change(Change(
                    change_type=ChangeType.NON_BREAKING,
                    severity=ChangeSeverity.INFO,
                    category="parameters",
                    path=param_path,
                    message=f"Parameter '{name}' is now optional (was required)",
                    old_value=old_required,
                    new_value=new_required,
                    impact_description="Parameter is now optional, providing more flexibility to clients"
                ))
        
        # Schema/type changes
        old_schema = old_param.get('schema', {})
        new_schema = new_param.get('schema', {})
        
        if old_schema != new_schema:
            self._compare_schemas(param_path, old_schema, new_schema, f"parameter '{name}'")
    
    def _compare_request_body(self, base_path: str, old_body: Optional[Dict], new_body: Optional[Dict]) -> None:
        """Compare request body specifications."""
        body_path = f"{base_path}/requestBody"
        
        # Request body added
        if old_body is None and new_body is not None:
            is_required = new_body.get('required', False)
            if is_required:
                self.report.add_change(Change(
                    change_type=ChangeType.BREAKING,
                    severity=ChangeSeverity.HIGH,
                    category="request_body",
                    path=body_path,
                    message="Required request body added",
                    old_value=None,
                    new_value=new_body,
                    migration_guide="Include request body with appropriate content type when calling this endpoint",
                    impact_description="Clients not providing request body will receive validation errors"
                ))
            else:
                self.report.add_change(Change(
                    change_type=ChangeType.NON_BREAKING,
                    severity=ChangeSeverity.INFO,
                    category="request_body",
                    path=body_path,
                    message="Optional request body added",
                    old_value=None,
                    new_value=new_body,
                    impact_description="Optional request body provides additional functionality"
                ))
        
        # Request body removed
        elif old_body is not None and new_body is None:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.HIGH,
                category="request_body",
                path=body_path,
                message="Request body removed",
                old_value=old_body,
                new_value=None,
                migration_guide="Remove request body when calling this endpoint",
                impact_description="Clients sending request body may receive validation errors"
            ))
        
        # Request body modified
        elif old_body is not None and new_body is not None:
            self._compare_request_body_details(body_path, old_body, new_body)
    
    def _compare_request_body_details(self, base_path: str, old_body: Dict, new_body: Dict) -> None:
        """Compare request body details."""
        # Required status change
        old_required = old_body.get('required', False)
        new_required = new_body.get('required', False)
        
        if old_required != new_required:
            if new_required:
                self.report.add_change(Change(
                    change_type=ChangeType.BREAKING,
                    severity=ChangeSeverity.HIGH,
                    category="request_body",
                    path=base_path,
                    message="Request body is now required (was optional)",
                    old_value=old_required,
                    new_value=new_required,
                    migration_guide="Always include request body when calling this endpoint",
                    impact_description="Clients not providing request body will receive validation errors"
                ))
            else:
                self.report.add_change(Change(
                    change_type=ChangeType.NON_BREAKING,
                    severity=ChangeSeverity.INFO,
                    category="request_body",
                    path=base_path,
                    message="Request body is now optional (was required)",
                    old_value=old_required,
                    new_value=new_required,
                    impact_description="Request body is now optional, providing more flexibility"
                ))
        
        # Content type changes
        old_content = old_body.get('content', {})
        new_content = new_body.get('content', {})
        self._compare_content_types(base_path, old_content, new_content, "request body")
    
    def _compare_responses(self, base_path: str, old_responses: Dict, new_responses: Dict) -> None:
        """Compare response specifications."""
        responses_path = f"{base_path}/responses"
        
        old_status_codes = set(old_responses.keys())
        new_status_codes = set(new_responses.keys())
        
        # Removed status codes
        removed_codes = old_status_codes - new_status_codes
        for code in removed_codes:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.HIGH,
                category="responses",
                path=f"{responses_path}/{code}",
                message=f"Response status code {code} removed",
                old_value=old_responses[code],
                new_value=None,
                migration_guide=f"Handle alternative status codes: {list(new_status_codes)}",
                impact_description=f"Clients expecting status code {code} need to handle different responses"
            ))
        
        # Added status codes
        added_codes = new_status_codes - old_status_codes
        for code in added_codes:
            self.report.add_change(Change(
                change_type=ChangeType.NON_BREAKING,
                severity=ChangeSeverity.INFO,
                category="responses",
                path=f"{responses_path}/{code}",
                message=f"New response status code {code} added",
                old_value=None,
                new_value=new_responses[code],
                impact_description="New status code provides more specific response information"
            ))
        
        # Modified responses
        common_codes = old_status_codes & new_status_codes
        for code in common_codes:
            self._compare_response_details(responses_path, code, old_responses[code], new_responses[code])
    
    def _compare_response_details(self, base_path: str, status_code: str, 
                                old_response: Dict, new_response: Dict) -> None:
        """Compare individual response details."""
        response_path = f"{base_path}/{status_code}"
        
        # Compare content types and schemas
        old_content = old_response.get('content', {})
        new_content = new_response.get('content', {})
        
        self._compare_content_types(response_path, old_content, new_content, f"response {status_code}")
    
    def _compare_content_types(self, base_path: str, old_content: Dict, new_content: Dict, context: str) -> None:
        """Compare content types and their schemas."""
        old_types = set(old_content.keys())
        new_types = set(new_content.keys())
        
        # Removed content types
        removed_types = old_types - new_types
        for content_type in removed_types:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.HIGH,
                category="content_types",
                path=f"{base_path}/content",
                message=f"Content type '{content_type}' removed from {context}",
                old_value=content_type,
                new_value=None,
                migration_guide=f"Use alternative content types: {list(new_types)}",
                impact_description=f"Clients expecting '{content_type}' need to handle different formats"
            ))
        
        # Added content types
        added_types = new_types - old_types
        for content_type in added_types:
            self.report.add_change(Change(
                change_type=ChangeType.ENHANCEMENT,
                severity=ChangeSeverity.INFO,
                category="content_types",
                path=f"{base_path}/content",
                message=f"New content type '{content_type}' added to {context}",
                old_value=None,
                new_value=content_type,
                impact_description=f"Additional format option available for {context}"
            ))
        
        # Modified schemas for common content types
        common_types = old_types & new_types
        for content_type in common_types:
            old_media = old_content[content_type]
            new_media = new_content[content_type]
            
            old_schema = old_media.get('schema', {})
            new_schema = new_media.get('schema', {})
            
            if old_schema != new_schema:
                schema_path = f"{base_path}/content/{content_type}/schema"
                self._compare_schemas(schema_path, old_schema, new_schema, f"{context} ({content_type})")
    
    def _compare_schemas(self, base_path: str, old_schema: Dict, new_schema: Dict, context: str) -> None:
        """Compare schema definitions."""
        # Type changes
        old_type = old_schema.get('type')
        new_type = new_schema.get('type')
        
        if old_type != new_type and old_type is not None and new_type is not None:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.CRITICAL,
                category="schema",
                path=base_path,
                message=f"Schema type changed from '{old_type}' to '{new_type}' for {context}",
                old_value=old_type,
                new_value=new_type,
                migration_guide=f"Update client code to handle {new_type} instead of {old_type}",
                impact_description="Type change will break client parsing and validation"
            ))
        
        # Property changes for object types
        if old_schema.get('type') == 'object' and new_schema.get('type') == 'object':
            self._compare_object_properties(base_path, old_schema, new_schema, context)
        
        # Array item changes
        if old_schema.get('type') == 'array' and new_schema.get('type') == 'array':
            old_items = old_schema.get('items', {})
            new_items = new_schema.get('items', {})
            if old_items != new_items:
                self._compare_schemas(f"{base_path}/items", old_items, new_items, f"{context} items")
    
    def _compare_object_properties(self, base_path: str, old_schema: Dict, new_schema: Dict, context: str) -> None:
        """Compare object schema properties."""
        old_props = old_schema.get('properties', {})
        new_props = new_schema.get('properties', {})
        old_required = set(old_schema.get('required', []))
        new_required = set(new_schema.get('required', []))
        
        old_prop_names = set(old_props.keys())
        new_prop_names = set(new_props.keys())
        
        # Removed properties
        removed_props = old_prop_names - new_prop_names
        for prop_name in removed_props:
            severity = ChangeSeverity.CRITICAL if prop_name in old_required else ChangeSeverity.HIGH
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=severity,
                category="schema",
                path=f"{base_path}/properties",
                message=f"Property '{prop_name}' removed from {context}",
                old_value=old_props[prop_name],
                new_value=None,
                migration_guide=f"Remove references to '{prop_name}' property in client code",
                impact_description="Clients expecting this property will receive incomplete data"
            ))
        
        # Added properties
        added_props = new_prop_names - old_prop_names
        for prop_name in added_props:
            if prop_name in new_required:
                # This is handled separately in required field changes
                pass
            else:
                self.report.add_change(Change(
                    change_type=ChangeType.NON_BREAKING,
                    severity=ChangeSeverity.INFO,
                    category="schema",
                    path=f"{base_path}/properties",
                    message=f"New optional property '{prop_name}' added to {context}",
                    old_value=None,
                    new_value=new_props[prop_name],
                    impact_description="New property provides additional data without breaking existing clients"
                ))
        
        # Required field changes
        added_required = new_required - old_required
        removed_required = old_required - new_required
        
        for prop_name in added_required:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.CRITICAL,
                category="schema",
                path=f"{base_path}/properties",
                message=f"Property '{prop_name}' is now required in {context}",
                old_value=False,
                new_value=True,
                migration_guide=f"Ensure '{prop_name}' is always provided when sending {context}",
                impact_description="Clients not providing this property will receive validation errors"
            ))
        
        for prop_name in removed_required:
            self.report.add_change(Change(
                change_type=ChangeType.NON_BREAKING,
                severity=ChangeSeverity.INFO,
                category="schema",
                path=f"{base_path}/properties",
                message=f"Property '{prop_name}' is no longer required in {context}",
                old_value=True,
                new_value=False,
                impact_description="Property is now optional, providing more flexibility"
            ))
        
        # Modified properties
        common_props = old_prop_names & new_prop_names
        for prop_name in common_props:
            old_prop = old_props[prop_name]
            new_prop = new_props[prop_name]
            if old_prop != new_prop:
                self._compare_schemas(f"{base_path}/properties/{prop_name}", 
                                    old_prop, new_prop, f"{context}.{prop_name}")
    
    def _compare_security_requirements(self, base_path: str, old_security: Optional[List], 
                                     new_security: Optional[List]) -> None:
        """Compare security requirements."""
        # Simplified security comparison - could be expanded
        if old_security != new_security:
            severity = ChangeSeverity.HIGH if new_security else ChangeSeverity.CRITICAL
            change_type = ChangeType.BREAKING
            
            if old_security is None and new_security is not None:
                message = "Security requirements added"
                migration_guide = "Ensure proper authentication/authorization when calling this endpoint"
                impact = "Endpoint now requires authentication"
            elif old_security is not None and new_security is None:
                message = "Security requirements removed"
                migration_guide = "Authentication is no longer required for this endpoint"
                impact = "Endpoint is now publicly accessible"
                severity = ChangeSeverity.MEDIUM  # Less severe, more permissive
            else:
                message = "Security requirements modified"
                migration_guide = "Update authentication/authorization method for this endpoint"
                impact = "Different authentication method required"
            
            self.report.add_change(Change(
                change_type=change_type,
                severity=severity,
                category="security",
                path=f"{base_path}/security",
                message=message,
                old_value=old_security,
                new_value=new_security,
                migration_guide=migration_guide,
                impact_description=impact
            ))
    
    def _compare_components_section(self) -> None:
        """Compare components sections."""
        old_components = self.old_spec.get('components', {})
        new_components = self.new_spec.get('components', {})
        
        # Compare schemas
        old_schemas = old_components.get('schemas', {})
        new_schemas = new_components.get('schemas', {})
        
        old_schema_names = set(old_schemas.keys())
        new_schema_names = set(new_schemas.keys())
        
        # Removed schemas
        removed_schemas = old_schema_names - new_schema_names
        for schema_name in removed_schemas:
            self.report.add_change(Change(
                change_type=ChangeType.BREAKING,
                severity=ChangeSeverity.HIGH,
                category="components",
                path=f"/components/schemas/{schema_name}",
                message=f"Schema '{schema_name}' removed from components",
                old_value=old_schemas[schema_name],
                new_value=None,
                migration_guide=f"Remove references to schema '{schema_name}' or use alternative schemas",
                impact_description="References to this schema will fail validation"
            ))
        
        # Added schemas
        added_schemas = new_schema_names - old_schema_names
        for schema_name in added_schemas:
            self.report.add_change(Change(
                change_type=ChangeType.ENHANCEMENT,
                severity=ChangeSeverity.INFO,
                category="components",
                path=f"/components/schemas/{schema_name}",
                message=f"New schema '{schema_name}' added to components",
                old_value=None,
                new_value=new_schemas[schema_name],
                impact_description="New reusable schema available"
            ))
        
        # Modified schemas
        common_schemas = old_schema_names & new_schema_names
        for schema_name in common_schemas:
            old_schema = old_schemas[schema_name]
            new_schema = new_schemas[schema_name]
            if old_schema != new_schema:
                self._compare_schemas(f"/components/schemas/{schema_name}", 
                                    old_schema, new_schema, f"schema '{schema_name}'")
    
    def _compare_security_section(self) -> None:
        """Compare security definitions."""
        old_security_schemes = self.old_spec.get('components', {}).get('securitySchemes', {})
        new_security_schemes = self.new_spec.get('components', {}).get('securitySchemes', {})
        
        if old_security_schemes != new_security_schemes:
            # Simplified comparison - could be more detailed
            self.report.add_change(Change(
                change_type=ChangeType.POTENTIALLY_BREAKING,
                severity=ChangeSeverity.MEDIUM,
                category="security",
                path="/components/securitySchemes",
                message="Security scheme definitions changed",
                old_value=old_security_schemes,
                new_value=new_security_schemes,
                migration_guide="Review authentication implementation for compatibility with new security schemes",
                impact_description="Authentication mechanisms may have changed"
            ))
    
    def _generate_endpoint_removal_migration(self, removed_path: str, method: str, 
                                           remaining_paths: Dict[str, Any]) -> str:
        """Generate migration guide for removed endpoints."""
        # Look for similar endpoints
        similar_paths = []
        path_segments = removed_path.strip('/').split('/')
        
        for existing_path in remaining_paths.keys():
            existing_segments = existing_path.strip('/').split('/')
            if len(existing_segments) == len(path_segments):
                # Check similarity
                similarity = sum(1 for i, seg in enumerate(path_segments) 
                               if i < len(existing_segments) and seg == existing_segments[i])
                if similarity >= len(path_segments) * 0.5:  # At least 50% similar
                    similar_paths.append(existing_path)
        
        if similar_paths:
            return f"Consider using alternative endpoints: {', '.join(similar_paths[:3])}"
        else:
            return "No direct replacement available. Review API documentation for alternative approaches."
    
    def _generate_method_removal_migration(self, path: str, removed_method: str, 
                                         remaining_methods: Set[str]) -> str:
        """Generate migration guide for removed HTTP methods."""
        method_alternatives = {
            'get': ['head'],
            'post': ['put', 'patch'],
            'put': ['post', 'patch'],
            'patch': ['put', 'post'],
            'delete': []
        }
        
        alternatives = []
        for alt_method in method_alternatives.get(removed_method.lower(), []):
            if alt_method in remaining_methods:
                alternatives.append(alt_method.upper())
        
        if alternatives:
            return f"Use alternative methods: {', '.join(alternatives)}"
        else:
            return f"No alternative HTTP methods available for {path}"
    
    def generate_json_report(self) -> str:
        """Generate JSON format report."""
        report_data = {
            "summary": self.report.summary,
            "hasBreakingChanges": self.report.has_breaking_changes(),
            "changes": [change.to_dict() for change in self.report.changes]
        }
        
        return json.dumps(report_data, indent=2)
    
    def generate_text_report(self) -> str:
        """Generate human-readable text report."""
        lines = [
            "═══════════════════════════════════════════════════════════════",
            "                  BREAKING CHANGE ANALYSIS REPORT",
            "═══════════════════════════════════════════════════════════════",
            "",
            "SUMMARY:",
            f"  Total Changes: {self.report.summary.get('total_changes', 0)}",
            f"  šŸ”“ Breaking Changes: {self.report.summary.get('breaking_changes', 0)}",
            f"  🟔 Potentially Breaking: {self.report.summary.get('potentially_breaking_changes', 0)}",
            f"  🟢 Non-Breaking Changes: {self.report.summary.get('non_breaking_changes', 0)}",
            f"  ✨ Enhancements: {self.report.summary.get('enhancements', 0)}",
            "",
            "SEVERITY BREAKDOWN:",
            f"  🚨 Critical: {self.report.summary.get('critical_severity', 0)}",
            f"  āš ļø  High: {self.report.summary.get('high_severity', 0)}",
            f"  ⚪ Medium: {self.report.summary.get('medium_severity', 0)}",
            f"  šŸ”µ Low: {self.report.summary.get('low_severity', 0)}",
            f"  ā„¹ļø  Info: {self.report.summary.get('info_severity', 0)}",
            ""
        ]
        
        if not self.report.changes:
            lines.extend([
                "šŸŽ‰ No changes detected between the API versions!",
                ""
            ])
        else:
            # Group changes by type and severity
            breaking_changes = [c for c in self.report.changes if c.change_type == ChangeType.BREAKING]
            potentially_breaking = [c for c in self.report.changes if c.change_type == ChangeType.POTENTIALLY_BREAKING]
            non_breaking = [c for c in self.report.changes if c.change_type == ChangeType.NON_BREAKING]
            enhancements = [c for c in self.report.changes if c.change_type == ChangeType.ENHANCEMENT]
            
            # Breaking changes section
            if breaking_changes:
                lines.extend([
                    "šŸ”“ BREAKING CHANGES:",
                    "═" * 60
                ])
                for change in sorted(breaking_changes, key=lambda x: x.severity.value):
                    self._add_change_to_report(lines, change)
                lines.append("")
            
            # Potentially breaking changes section
            if potentially_breaking:
                lines.extend([
                    "🟔 POTENTIALLY BREAKING CHANGES:",
                    "═" * 60
                ])
                for change in sorted(potentially_breaking, key=lambda x: x.severity.value):
                    self._add_change_to_report(lines, change)
                lines.append("")
            
            # Non-breaking changes section
            if non_breaking:
                lines.extend([
                    "🟢 NON-BREAKING CHANGES:",
                    "═" * 60
                ])
                for change in non_breaking:
                    self._add_change_to_report(lines, change)
                lines.append("")
            
            # Enhancements section
            if enhancements:
                lines.extend([
                    "✨ ENHANCEMENTS:",
                    "═" * 60
                ])
                for change in enhancements:
                    self._add_change_to_report(lines, change)
                lines.append("")
        
        # Add overall assessment
        lines.extend([
            "═══════════════════════════════════════════════════════════════",
            "OVERALL ASSESSMENT:",
            "═══════════════════════════════════════════════════════════════"
        ])
        
        if self.report.has_breaking_changes():
            breaking_count = self.report.summary.get('breaking_changes', 0)
            potentially_breaking_count = self.report.summary.get('potentially_breaking_changes', 0)
            
            if breaking_count > 0:
                lines.extend([
                    f"ā›” MAJOR VERSION BUMP REQUIRED",
                    f"   This API version contains {breaking_count} breaking changes that will",
                    f"   definitely break existing clients. A major version bump is required.",
                    ""
                ])
            elif potentially_breaking_count > 0:
                lines.extend([
                    f"āš ļø  MINOR VERSION BUMP RECOMMENDED",
                    f"   This API version contains {potentially_breaking_count} potentially breaking",
                    f"   changes. Consider a minor version bump and communicate changes to clients.",
                    ""
                ])
        else:
            lines.extend([
                "āœ… PATCH VERSION BUMP ACCEPTABLE",
                "   No breaking changes detected. This version is backward compatible",
                "   with existing clients.",
                ""
            ])
        
        return "\n".join(lines)
    
    def _add_change_to_report(self, lines: List[str], change: Change) -> None:
        """Add a change to the text report."""
        severity_icons = {
            ChangeSeverity.CRITICAL: "🚨",
            ChangeSeverity.HIGH: "āš ļø ",
            ChangeSeverity.MEDIUM: "⚪",
            ChangeSeverity.LOW: "šŸ”µ",
            ChangeSeverity.INFO: "ā„¹ļø "
        }
        
        icon = severity_icons.get(change.severity, "ā“")
        
        lines.extend([
            f"{icon} {change.severity.value.upper()}: {change.message}",
            f"   Path: {change.path}",
            f"   Category: {change.category}"
        ])
        
        if change.impact_description:
            lines.append(f"   Impact: {change.impact_description}")
        
        if change.migration_guide:
            lines.append(f"   šŸ’” Migration: {change.migration_guide}")
        
        lines.append("")


def main():
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        description="Compare API specification versions to detect breaking changes",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python breaking_change_detector.py v1.json v2.json
  python breaking_change_detector.py --format json v1.json v2.json > changes.json
  python breaking_change_detector.py --output report.txt v1.json v2.json
        """
    )
    
    parser.add_argument(
        'old_spec',
        help='Old API specification file (JSON format)'
    )
    
    parser.add_argument(
        'new_spec',
        help='New API specification file (JSON format)'
    )
    
    parser.add_argument(
        '--format',
        choices=['text', 'json'],
        default='text',
        help='Output format (default: text)'
    )
    
    parser.add_argument(
        '--output',
        help='Output file (default: stdout)'
    )
    
    parser.add_argument(
        '--exit-on-breaking',
        action='store_true',
        help='Exit with code 1 if breaking changes are detected'
    )
    
    args = parser.parse_args()
    
    # Load specification files
    try:
        with open(args.old_spec, 'r') as f:
            old_spec = json.load(f)
    except FileNotFoundError:
        print(f"Error: Old specification file '{args.old_spec}' not found.", file=sys.stderr)
        return 1
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in '{args.old_spec}': {e}", file=sys.stderr)
        return 1
    
    try:
        with open(args.new_spec, 'r') as f:
            new_spec = json.load(f)
    except FileNotFoundError:
        print(f"Error: New specification file '{args.new_spec}' not found.", file=sys.stderr)
        return 1
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in '{args.new_spec}': {e}", file=sys.stderr)
        return 1
    
    # Initialize detector and compare specifications
    detector = BreakingChangeDetector()
    
    try:
        report = detector.compare_specs(old_spec, new_spec)
    except Exception as e:
        print(f"Error during comparison: {e}", file=sys.stderr)
        return 1
    
    # Generate report
    if args.format == 'json':
        output = detector.generate_json_report()
    else:
        output = detector.generate_text_report()
    
    # Write output
    if args.output:
        try:
            with open(args.output, 'w') as f:
                f.write(output)
            print(f"Breaking change report written to {args.output}")
        except IOError as e:
            print(f"Error writing to '{args.output}': {e}", file=sys.stderr)
            return 1
    else:
        print(output)
    
    # Exit with appropriate code
    if args.exit_on_breaking and report.has_breaking_changes():
        return 1
    
    return 0


if __name__ == '__main__':
    sys.exit(main())

scripts/api_scorecard.py

#!/usr/bin/env python3
"""
API Scorecard - Comprehensive API design quality assessment tool.

This script evaluates API designs across multiple dimensions and generates
a detailed scorecard with letter grades and improvement recommendations.

Scoring Dimensions:
- Consistency (30%): Naming conventions, response patterns, structural consistency
- Documentation (20%): Completeness and clarity of API documentation  
- Security (20%): Authentication, authorization, and security best practices
- Usability (15%): Ease of use, discoverability, and developer experience
- Performance (15%): Caching, pagination, and efficiency patterns

Generates letter grades (A-F) with detailed breakdowns and actionable recommendations.
"""

import argparse
import json
import re
import sys
from typing import Any, Dict, List, Optional, Set, Tuple
from dataclasses import dataclass, field
from enum import Enum
import math


class ScoreCategory(Enum):
    """Scoring categories."""
    CONSISTENCY = "consistency"
    DOCUMENTATION = "documentation"
    SECURITY = "security"
    USABILITY = "usability"
    PERFORMANCE = "performance"


@dataclass
class CategoryScore:
    """Score for a specific category."""
    category: ScoreCategory
    score: float  # 0-100
    max_score: float  # Usually 100
    weight: float  # Percentage weight in overall score
    issues: List[str] = field(default_factory=list)
    recommendations: List[str] = field(default_factory=list)
    
    @property
    def letter_grade(self) -> str:
        """Convert score to letter grade."""
        if self.score >= 90:
            return "A"
        elif self.score >= 80:
            return "B"
        elif self.score >= 70:
            return "C"
        elif self.score >= 60:
            return "D"
        else:
            return "F"
    
    @property
    def weighted_score(self) -> float:
        """Calculate weighted contribution to overall score."""
        return (self.score / 100.0) * self.weight


@dataclass
class APIScorecard:
    """Complete API scorecard with all category scores."""
    category_scores: Dict[ScoreCategory, CategoryScore] = field(default_factory=dict)
    overall_score: float = 0.0
    overall_grade: str = "F"
    total_endpoints: int = 0
    api_info: Dict[str, Any] = field(default_factory=dict)
    
    def calculate_overall_score(self) -> None:
        """Calculate overall weighted score and grade."""
        self.overall_score = sum(score.weighted_score for score in self.category_scores.values())
        
        if self.overall_score >= 90:
            self.overall_grade = "A"
        elif self.overall_score >= 80:
            self.overall_grade = "B"
        elif self.overall_score >= 70:
            self.overall_grade = "C"
        elif self.overall_score >= 60:
            self.overall_grade = "D"
        else:
            self.overall_grade = "F"
    
    def get_top_recommendations(self, limit: int = 5) -> List[str]:
        """Get top recommendations across all categories."""
        all_recommendations = []
        for category_score in self.category_scores.values():
            for rec in category_score.recommendations:
                all_recommendations.append(f"{category_score.category.value.title()}: {rec}")
        
        # Sort by category weight (highest impact first)
        weighted_recs = []
        for category_score in sorted(self.category_scores.values(), 
                                   key=lambda x: x.weight, reverse=True):
            for rec in category_score.recommendations[:2]:  # Top 2 per category
                weighted_recs.append(f"{category_score.category.value.title()}: {rec}")
        
        return weighted_recs[:limit]


class APIScoringEngine:
    """Main API scoring engine."""
    
    def __init__(self):
        self.scorecard = APIScorecard()
        self.spec: Optional[Dict] = None
        
        # Regex patterns for validation
        self.kebab_case_pattern = re.compile(r'^[a-z]+(?:-[a-z0-9]+)*$')
        self.camel_case_pattern = re.compile(r'^[a-z][a-zA-Z0-9]*$')
        self.pascal_case_pattern = re.compile(r'^[A-Z][a-zA-Z0-9]*$')
        
        # HTTP methods
        self.http_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}
        
        # Category weights (must sum to 100)
        self.category_weights = {
            ScoreCategory.CONSISTENCY: 30.0,
            ScoreCategory.DOCUMENTATION: 20.0,
            ScoreCategory.SECURITY: 20.0,
            ScoreCategory.USABILITY: 15.0,
            ScoreCategory.PERFORMANCE: 15.0
        }
    
    def score_api(self, spec: Dict[str, Any]) -> APIScorecard:
        """Generate comprehensive API scorecard."""
        self.spec = spec
        self.scorecard = APIScorecard()
        
        # Extract basic API info
        self._extract_api_info()
        
        # Score each category
        self._score_consistency()
        self._score_documentation()
        self._score_security()
        self._score_usability()
        self._score_performance()
        
        # Calculate overall score
        self.scorecard.calculate_overall_score()
        
        return self.scorecard
    
    def _extract_api_info(self) -> None:
        """Extract basic API information."""
        info = self.spec.get('info', {})
        paths = self.spec.get('paths', {})
        
        self.scorecard.api_info = {
            'title': info.get('title', 'Unknown API'),
            'version': info.get('version', ''),
            'description': info.get('description', ''),
            'total_paths': len(paths),
            'openapi_version': self.spec.get('openapi', self.spec.get('swagger', ''))
        }
        
        # Count total endpoints
        endpoint_count = 0
        for path_obj in paths.values():
            if isinstance(path_obj, dict):
                endpoint_count += len([m for m in path_obj.keys() 
                                     if m.upper() in self.http_methods])
        
        self.scorecard.total_endpoints = endpoint_count
    
    def _score_consistency(self) -> None:
        """Score API consistency (30% weight)."""
        category = ScoreCategory.CONSISTENCY
        score = CategoryScore(
            category=category,
            score=0.0,
            max_score=100.0,
            weight=self.category_weights[category]
        )
        
        consistency_checks = [
            self._check_naming_consistency(),
            self._check_response_consistency(),
            self._check_error_format_consistency(),
            self._check_parameter_consistency(),
            self._check_url_structure_consistency(),
            self._check_http_method_consistency(),
            self._check_status_code_consistency()
        ]
        
        # Average the consistency scores
        valid_scores = [s for s in consistency_checks if s is not None]
        if valid_scores:
            score.score = sum(valid_scores) / len(valid_scores)
        
        # Add specific recommendations based on low scores
        if score.score < 70:
            score.recommendations.extend([
                "Review naming conventions across all endpoints and schemas",
                "Standardize response formats and error structures",
                "Ensure consistent HTTP method usage patterns"
            ])
        elif score.score < 85:
            score.recommendations.extend([
                "Minor consistency improvements needed in naming or response formats",
                "Consider creating API design guidelines document"
            ])
        
        self.scorecard.category_scores[category] = score
    
    def _check_naming_consistency(self) -> float:
        """Check naming convention consistency."""
        paths = self.spec.get('paths', {})
        schemas = self.spec.get('components', {}).get('schemas', {})
        
        total_checks = 0
        passed_checks = 0
        
        # Check path naming (should be kebab-case)
        for path in paths.keys():
            segments = [seg for seg in path.split('/') if seg and not seg.startswith('{')]
            for segment in segments:
                total_checks += 1
                if self.kebab_case_pattern.match(segment) or re.match(r'^v\d+$', segment):
                    passed_checks += 1
        
        # Check schema naming (should be PascalCase)
        for schema_name in schemas.keys():
            total_checks += 1
            if self.pascal_case_pattern.match(schema_name):
                passed_checks += 1
        
        # Check property naming within schemas
        for schema in schemas.values():
            if isinstance(schema, dict) and 'properties' in schema:
                for prop_name in schema['properties'].keys():
                    total_checks += 1
                    if self.camel_case_pattern.match(prop_name):
                        passed_checks += 1
        
        return (passed_checks / total_checks * 100) if total_checks > 0 else 100
    
    def _check_response_consistency(self) -> float:
        """Check response format consistency."""
        paths = self.spec.get('paths', {})
        
        response_patterns = []
        total_responses = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods or not isinstance(operation, dict):
                    continue
                
                responses = operation.get('responses', {})
                for status_code, response in responses.items():
                    if not isinstance(response, dict):
                        continue
                        
                    total_responses += 1
                    content = response.get('content', {})
                    
                    # Analyze response structure
                    for media_type, media_obj in content.items():
                        schema = media_obj.get('schema', {})
                        pattern = self._extract_schema_pattern(schema)
                        response_patterns.append(pattern)
        
        # Calculate consistency by comparing patterns
        if not response_patterns:
            return 100
        
        pattern_counts = {}
        for pattern in response_patterns:
            pattern_key = json.dumps(pattern, sort_keys=True)
            pattern_counts[pattern_key] = pattern_counts.get(pattern_key, 0) + 1
        
        # Most common pattern should dominate for good consistency
        max_count = max(pattern_counts.values()) if pattern_counts else 0
        consistency_ratio = max_count / len(response_patterns) if response_patterns else 1
        
        return consistency_ratio * 100
    
    def _extract_schema_pattern(self, schema: Dict[str, Any]) -> Dict[str, Any]:
        """Extract a pattern from a schema for consistency checking."""
        if not isinstance(schema, dict):
            return {}
        
        pattern = {
            'type': schema.get('type'),
            'has_properties': 'properties' in schema,
            'has_items': 'items' in schema,
            'required_count': len(schema.get('required', [])),
            'property_count': len(schema.get('properties', {}))
        }
        
        return pattern
    
    def _check_error_format_consistency(self) -> float:
        """Check error response format consistency."""
        paths = self.spec.get('paths', {})
        error_responses = []
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                responses = operation.get('responses', {})
                for status_code, response in responses.items():
                    try:
                        code_int = int(status_code)
                        if code_int >= 400:  # Error responses
                            content = response.get('content', {})
                            for media_type, media_obj in content.items():
                                schema = media_obj.get('schema', {})
                                error_responses.append(self._extract_schema_pattern(schema))
                    except ValueError:
                        continue
        
        if not error_responses:
            return 80  # No error responses defined - somewhat concerning
        
        # Check consistency of error response formats
        pattern_counts = {}
        for pattern in error_responses:
            pattern_key = json.dumps(pattern, sort_keys=True)
            pattern_counts[pattern_key] = pattern_counts.get(pattern_key, 0) + 1
        
        max_count = max(pattern_counts.values()) if pattern_counts else 0
        consistency_ratio = max_count / len(error_responses) if error_responses else 1
        
        return consistency_ratio * 100
    
    def _check_parameter_consistency(self) -> float:
        """Check parameter naming and usage consistency."""
        paths = self.spec.get('paths', {})
        
        query_params = []
        path_params = []
        header_params = []
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                parameters = operation.get('parameters', [])
                for param in parameters:
                    if not isinstance(param, dict):
                        continue
                        
                    param_name = param.get('name', '')
                    param_in = param.get('in', '')
                    
                    if param_in == 'query':
                        query_params.append(param_name)
                    elif param_in == 'path':
                        path_params.append(param_name)
                    elif param_in == 'header':
                        header_params.append(param_name)
        
        # Check naming consistency for each parameter type
        scores = []
        
        # Query parameters should be camelCase or kebab-case
        if query_params:
            valid_query = sum(1 for p in query_params 
                            if self.camel_case_pattern.match(p) or self.kebab_case_pattern.match(p))
            scores.append((valid_query / len(query_params)) * 100)
        
        # Path parameters should be camelCase or kebab-case
        if path_params:
            valid_path = sum(1 for p in path_params 
                           if self.camel_case_pattern.match(p) or self.kebab_case_pattern.match(p))
            scores.append((valid_path / len(path_params)) * 100)
        
        return sum(scores) / len(scores) if scores else 100
    
    def _check_url_structure_consistency(self) -> float:
        """Check URL structure and pattern consistency."""
        paths = self.spec.get('paths', {})
        
        total_paths = len(paths)
        if total_paths == 0:
            return 0
        
        structure_score = 0
        
        # Check for consistent versioning
        versioned_paths = 0
        for path in paths.keys():
            if re.search(r'/v\d+/', path):
                versioned_paths += 1
        
        # Either all or none should be versioned for consistency
        if versioned_paths == 0 or versioned_paths == total_paths:
            structure_score += 25
        elif versioned_paths > total_paths * 0.8:
            structure_score += 20
        
        # Check for reasonable path depth
        reasonable_depth = 0
        for path in paths.keys():
            segments = [seg for seg in path.split('/') if seg]
            if 2 <= len(segments) <= 5:  # Reasonable depth
                reasonable_depth += 1
        
        structure_score += (reasonable_depth / total_paths) * 25
        
        # Check for RESTful resource patterns
        restful_patterns = 0
        for path in paths.keys():
            # Look for patterns like /resources/{id} or /resources
            if re.match(r'^/[a-z-]+(/\{[^}]+\})?(/[a-z-]+)*$', path):
                restful_patterns += 1
        
        structure_score += (restful_patterns / total_paths) * 30
        
        # Check for consistent trailing slash usage
        with_slash = sum(1 for path in paths.keys() if path.endswith('/'))
        without_slash = total_paths - with_slash
        
        # Either all or none should have trailing slashes
        if with_slash == 0 or without_slash == 0:
            structure_score += 20
        elif min(with_slash, without_slash) < total_paths * 0.1:
            structure_score += 15
        
        return min(structure_score, 100)
    
    def _check_http_method_consistency(self) -> float:
        """Check HTTP method usage consistency."""
        paths = self.spec.get('paths', {})
        
        method_usage = {}
        total_operations = 0
        
        for path, path_obj in paths.items():
            if not isinstance(path_obj, dict):
                continue
                
            for method in path_obj.keys():
                if method.upper() in self.http_methods:
                    method_upper = method.upper()
                    total_operations += 1
                    
                    # Analyze method usage patterns
                    if method_upper not in method_usage:
                        method_usage[method_upper] = {'count': 0, 'appropriate': 0}
                    
                    method_usage[method_upper]['count'] += 1
                    
                    # Check if method usage seems appropriate
                    if self._is_method_usage_appropriate(path, method_upper, path_obj[method]):
                        method_usage[method_upper]['appropriate'] += 1
        
        if total_operations == 0:
            return 0
        
        # Calculate appropriateness score
        total_appropriate = sum(data['appropriate'] for data in method_usage.values())
        return (total_appropriate / total_operations) * 100
    
    def _is_method_usage_appropriate(self, path: str, method: str, operation: Dict) -> bool:
        """Check if HTTP method usage is appropriate for the endpoint."""
        # Simple heuristics for method appropriateness
        has_request_body = 'requestBody' in operation
        path_has_id = '{' in path and '}' in path
        
        if method == 'GET':
            return not has_request_body  # GET should not have body
        elif method == 'POST':
            return not path_has_id  # POST typically for collections
        elif method == 'PUT':
            return path_has_id and has_request_body  # PUT for specific resources
        elif method == 'PATCH':
            return path_has_id  # PATCH for specific resources
        elif method == 'DELETE':
            return path_has_id  # DELETE for specific resources
        
        return True  # Default to appropriate for other methods
    
    def _check_status_code_consistency(self) -> float:
        """Check HTTP status code usage consistency."""
        paths = self.spec.get('paths', {})
        
        method_status_patterns = {}
        total_operations = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                total_operations += 1
                responses = operation.get('responses', {})
                status_codes = set(responses.keys())
                
                if method.upper() not in method_status_patterns:
                    method_status_patterns[method.upper()] = []
                
                method_status_patterns[method.upper()].append(status_codes)
        
        if total_operations == 0:
            return 0
        
        # Check consistency within each method type
        consistency_scores = []
        
        for method, status_patterns in method_status_patterns.items():
            if not status_patterns:
                continue
            
            # Find common status codes for this method
            all_codes = set()
            for pattern in status_patterns:
                all_codes.update(pattern)
            
            # Calculate how many operations use the most common codes
            code_usage = {}
            for code in all_codes:
                code_usage[code] = sum(1 for pattern in status_patterns if code in pattern)
            
            # Score based on consistency of common status codes
            if status_patterns:
                avg_consistency = sum(
                    len([code for code in pattern if code_usage.get(code, 0) > len(status_patterns) * 0.5]) 
                    for pattern in status_patterns
                ) / len(status_patterns)
                
                method_consistency = min(avg_consistency / 3.0 * 100, 100)  # Expect ~3 common codes
                consistency_scores.append(method_consistency)
        
        return sum(consistency_scores) / len(consistency_scores) if consistency_scores else 100
    
    def _score_documentation(self) -> None:
        """Score API documentation quality (20% weight)."""
        category = ScoreCategory.DOCUMENTATION
        score = CategoryScore(
            category=category,
            score=0.0,
            max_score=100.0,
            weight=self.category_weights[category]
        )
        
        documentation_checks = [
            self._check_api_level_documentation(),
            self._check_endpoint_documentation(),
            self._check_schema_documentation(),
            self._check_parameter_documentation(),
            self._check_response_documentation(),
            self._check_example_coverage()
        ]
        
        valid_scores = [s for s in documentation_checks if s is not None]
        if valid_scores:
            score.score = sum(valid_scores) / len(valid_scores)
        
        # Add recommendations based on score
        if score.score < 60:
            score.recommendations.extend([
                "Add comprehensive descriptions to all API components",
                "Include examples for complex operations and schemas",
                "Document all parameters and response fields"
            ])
        elif score.score < 80:
            score.recommendations.extend([
                "Improve documentation completeness for some endpoints",
                "Add more examples to enhance developer experience"
            ])
        
        self.scorecard.category_scores[category] = score
    
    def _check_api_level_documentation(self) -> float:
        """Check API-level documentation completeness."""
        info = self.spec.get('info', {})
        score = 0
        
        # Required fields
        if info.get('title'):
            score += 20
        if info.get('version'):
            score += 20
        if info.get('description') and len(info['description']) > 20:
            score += 30
        
        # Optional but recommended fields
        if info.get('contact'):
            score += 15
        if info.get('license'):
            score += 15
        
        return score
    
    def _check_endpoint_documentation(self) -> float:
        """Check endpoint-level documentation completeness."""
        paths = self.spec.get('paths', {})
        
        total_operations = 0
        documented_operations = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                total_operations += 1
                doc_score = 0
                
                if operation.get('summary'):
                    doc_score += 1
                if operation.get('description') and len(operation['description']) > 20:
                    doc_score += 1
                if operation.get('operationId'):
                    doc_score += 1
                
                # Consider it documented if it has at least 2/3 elements
                if doc_score >= 2:
                    documented_operations += 1
        
        return (documented_operations / total_operations * 100) if total_operations > 0 else 100
    
    def _check_schema_documentation(self) -> float:
        """Check schema documentation completeness."""
        schemas = self.spec.get('components', {}).get('schemas', {})
        
        if not schemas:
            return 80  # No schemas to document
        
        total_schemas = len(schemas)
        documented_schemas = 0
        
        for schema_name, schema in schemas.items():
            if not isinstance(schema, dict):
                continue
            
            doc_elements = 0
            
            # Schema-level description
            if schema.get('description'):
                doc_elements += 1
            
            # Property descriptions
            properties = schema.get('properties', {})
            if properties:
                described_props = sum(1 for prop in properties.values() 
                                    if isinstance(prop, dict) and prop.get('description'))
                if described_props > len(properties) * 0.5:  # At least 50% documented
                    doc_elements += 1
            
            # Examples
            if schema.get('example') or any(
                isinstance(prop, dict) and prop.get('example') 
                for prop in properties.values()
            ):
                doc_elements += 1
            
            if doc_elements >= 2:
                documented_schemas += 1
        
        return (documented_schemas / total_schemas * 100) if total_schemas > 0 else 100
    
    def _check_parameter_documentation(self) -> float:
        """Check parameter documentation completeness."""
        paths = self.spec.get('paths', {})
        
        total_params = 0
        documented_params = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                parameters = operation.get('parameters', [])
                for param in parameters:
                    if not isinstance(param, dict):
                        continue
                    
                    total_params += 1
                    
                    doc_score = 0
                    if param.get('description'):
                        doc_score += 1
                    if param.get('example') or (param.get('schema', {}).get('example')):
                        doc_score += 1
                    
                    if doc_score >= 1:  # At least description
                        documented_params += 1
        
        return (documented_params / total_params * 100) if total_params > 0 else 100
    
    def _check_response_documentation(self) -> float:
        """Check response documentation completeness."""
        paths = self.spec.get('paths', {})
        
        total_responses = 0
        documented_responses = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                responses = operation.get('responses', {})
                for status_code, response in responses.items():
                    if not isinstance(response, dict):
                        continue
                    
                    total_responses += 1
                    
                    if response.get('description'):
                        documented_responses += 1
        
        return (documented_responses / total_responses * 100) if total_responses > 0 else 100
    
    def _check_example_coverage(self) -> float:
        """Check example coverage across the API."""
        paths = self.spec.get('paths', {})
        schemas = self.spec.get('components', {}).get('schemas', {})
        
        # Check examples in operations
        total_operations = 0
        operations_with_examples = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                total_operations += 1
                
                has_example = False
                
                # Check request body examples
                request_body = operation.get('requestBody', {})
                if self._has_examples(request_body.get('content', {})):
                    has_example = True
                
                # Check response examples
                responses = operation.get('responses', {})
                for response in responses.values():
                    if isinstance(response, dict) and self._has_examples(response.get('content', {})):
                        has_example = True
                        break
                
                if has_example:
                    operations_with_examples += 1
        
        # Check examples in schemas
        total_schemas = len(schemas)
        schemas_with_examples = 0
        
        for schema in schemas.values():
            if isinstance(schema, dict) and self._schema_has_examples(schema):
                schemas_with_examples += 1
        
        # Combine scores
        operation_score = (operations_with_examples / total_operations * 100) if total_operations > 0 else 100
        schema_score = (schemas_with_examples / total_schemas * 100) if total_schemas > 0 else 100
        
        return (operation_score + schema_score) / 2
    
    def _has_examples(self, content: Dict[str, Any]) -> bool:
        """Check if content has examples."""
        for media_type, media_obj in content.items():
            if isinstance(media_obj, dict):
                if media_obj.get('example') or media_obj.get('examples'):
                    return True
        return False
    
    def _schema_has_examples(self, schema: Dict[str, Any]) -> bool:
        """Check if schema has examples."""
        if schema.get('example'):
            return True
        
        properties = schema.get('properties', {})
        for prop in properties.values():
            if isinstance(prop, dict) and prop.get('example'):
                return True
        
        return False
    
    def _score_security(self) -> None:
        """Score API security implementation (20% weight)."""
        category = ScoreCategory.SECURITY
        score = CategoryScore(
            category=category,
            score=0.0,
            max_score=100.0,
            weight=self.category_weights[category]
        )
        
        security_checks = [
            self._check_security_schemes(),
            self._check_security_requirements(),
            self._check_https_usage(),
            self._check_authentication_patterns(),
            self._check_sensitive_data_handling()
        ]
        
        valid_scores = [s for s in security_checks if s is not None]
        if valid_scores:
            score.score = sum(valid_scores) / len(valid_scores)
        
        # Add recommendations
        if score.score < 50:
            score.recommendations.extend([
                "Implement comprehensive security schemes (OAuth2, API keys, etc.)",
                "Ensure all endpoints have appropriate security requirements",
                "Add input validation and rate limiting patterns"
            ])
        elif score.score < 80:
            score.recommendations.extend([
                "Review security coverage for all endpoints",
                "Consider additional security measures for sensitive operations"
            ])
        
        self.scorecard.category_scores[category] = score
    
    def _check_security_schemes(self) -> float:
        """Check security scheme definitions."""
        security_schemes = self.spec.get('components', {}).get('securitySchemes', {})
        
        if not security_schemes:
            return 20  # Very low score for no security
        
        score = 40  # Base score for having security schemes
        
        scheme_types = set()
        for scheme in security_schemes.values():
            if isinstance(scheme, dict):
                scheme_type = scheme.get('type')
                scheme_types.add(scheme_type)
        
        # Bonus for modern security schemes
        if 'oauth2' in scheme_types:
            score += 30
        if 'apiKey' in scheme_types:
            score += 15
        if 'http' in scheme_types:
            score += 15
        
        return min(score, 100)
    
    def _check_security_requirements(self) -> float:
        """Check security requirement coverage."""
        paths = self.spec.get('paths', {})
        global_security = self.spec.get('security', [])
        
        total_operations = 0
        secured_operations = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                total_operations += 1
                
                # Check if operation has security requirements
                operation_security = operation.get('security')
                
                if operation_security is not None:
                    secured_operations += 1
                elif global_security:
                    secured_operations += 1
        
        return (secured_operations / total_operations * 100) if total_operations > 0 else 0
    
    def _check_https_usage(self) -> float:
        """Check HTTPS enforcement."""
        servers = self.spec.get('servers', [])
        
        if not servers:
            return 60  # No servers defined - assume HTTPS
        
        https_servers = 0
        for server in servers:
            if isinstance(server, dict):
                url = server.get('url', '')
                if url.startswith('https://') or not url.startswith('http://'):
                    https_servers += 1
        
        return (https_servers / len(servers) * 100) if servers else 100
    
    def _check_authentication_patterns(self) -> float:
        """Check authentication pattern quality."""
        security_schemes = self.spec.get('components', {}).get('securitySchemes', {})
        
        if not security_schemes:
            return 0
        
        pattern_scores = []
        
        for scheme in security_schemes.values():
            if not isinstance(scheme, dict):
                continue
            
            scheme_type = scheme.get('type', '').lower()
            
            if scheme_type == 'oauth2':
                # OAuth2 is highly recommended
                flows = scheme.get('flows', {})
                if flows:
                    pattern_scores.append(95)
                else:
                    pattern_scores.append(80)
            elif scheme_type == 'http':
                scheme_scheme = scheme.get('scheme', '').lower()
                if scheme_scheme == 'bearer':
                    pattern_scores.append(85)
                elif scheme_scheme == 'basic':
                    pattern_scores.append(60)  # Less secure
                else:
                    pattern_scores.append(70)
            elif scheme_type == 'apikey':
                location = scheme.get('in', '').lower()
                if location == 'header':
                    pattern_scores.append(75)
                else:
                    pattern_scores.append(60)  # Query/cookie less secure
            else:
                pattern_scores.append(50)  # Unknown scheme
        
        return sum(pattern_scores) / len(pattern_scores) if pattern_scores else 0
    
    def _check_sensitive_data_handling(self) -> float:
        """Check sensitive data handling patterns."""
        # This is a simplified check - in reality would need more sophisticated analysis
        schemas = self.spec.get('components', {}).get('schemas', {})
        
        score = 80  # Default good score
        
        # Look for potential sensitive fields without proper handling
        sensitive_field_names = {'password', 'secret', 'token', 'key', 'ssn', 'credit_card'}
        
        for schema in schemas.values():
            if not isinstance(schema, dict):
                continue
            
            properties = schema.get('properties', {})
            for prop_name, prop_def in properties.items():
                if not isinstance(prop_def, dict):
                    continue
                
                # Check for sensitive field names
                if any(sensitive in prop_name.lower() for sensitive in sensitive_field_names):
                    # Check if it's marked as sensitive (writeOnly, format: password, etc.)
                    if not (prop_def.get('writeOnly') or 
                           prop_def.get('format') == 'password' or
                           'password' in prop_def.get('description', '').lower()):
                        score -= 10  # Penalty for exposed sensitive field
        
        return max(score, 0)
    
    def _score_usability(self) -> None:
        """Score API usability and developer experience (15% weight)."""
        category = ScoreCategory.USABILITY
        score = CategoryScore(
            category=category,
            score=0.0,
            max_score=100.0,
            weight=self.category_weights[category]
        )
        
        usability_checks = [
            self._check_discoverability(),
            self._check_error_handling(),
            self._check_filtering_and_searching(),
            self._check_resource_relationships(),
            self._check_developer_experience()
        ]
        
        valid_scores = [s for s in usability_checks if s is not None]
        if valid_scores:
            score.score = sum(valid_scores) / len(valid_scores)
        
        # Add recommendations
        if score.score < 60:
            score.recommendations.extend([
                "Improve error messages with actionable guidance",
                "Add filtering and search capabilities to list endpoints",
                "Enhance resource discoverability with better linking"
            ])
        elif score.score < 80:
            score.recommendations.extend([
                "Consider adding HATEOAS links for better discoverability",
                "Enhance developer experience with better examples"
            ])
        
        self.scorecard.category_scores[category] = score
    
    def _check_discoverability(self) -> float:
        """Check API discoverability features."""
        paths = self.spec.get('paths', {})
        
        # Look for root/discovery endpoints
        has_root = '/' in paths or any(path == '/api' or path.startswith('/api/') for path in paths)
        
        # Look for HATEOAS patterns in responses
        hateoas_score = 0
        total_responses = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                responses = operation.get('responses', {})
                for response in responses.values():
                    if not isinstance(response, dict):
                        continue
                    
                    total_responses += 1
                    
                    # Look for link-like properties in response schemas
                    content = response.get('content', {})
                    for media_obj in content.values():
                        schema = media_obj.get('schema', {})
                        if self._has_link_properties(schema):
                            hateoas_score += 1
                            break
        
        discovery_score = 50 if has_root else 30
        if total_responses > 0:
            hateoas_ratio = hateoas_score / total_responses
            discovery_score += hateoas_ratio * 50
        
        return min(discovery_score, 100)
    
    def _has_link_properties(self, schema: Dict[str, Any]) -> bool:
        """Check if schema has link-like properties."""
        if not isinstance(schema, dict):
            return False
        
        properties = schema.get('properties', {})
        link_indicators = {'links', '_links', 'href', 'url', 'self', 'next', 'prev'}
        
        return any(prop_name.lower() in link_indicators for prop_name in properties.keys())
    
    def _check_error_handling(self) -> float:
        """Check error handling quality."""
        paths = self.spec.get('paths', {})
        
        total_operations = 0
        operations_with_errors = 0
        detailed_error_responses = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                total_operations += 1
                responses = operation.get('responses', {})
                
                # Check for error responses
                has_error_responses = any(
                    status_code.startswith('4') or status_code.startswith('5')
                    for status_code in responses.keys()
                )
                
                if has_error_responses:
                    operations_with_errors += 1
                    
                    # Check for detailed error schemas
                    for status_code, response in responses.items():
                        if (status_code.startswith('4') or status_code.startswith('5')) and isinstance(response, dict):
                            content = response.get('content', {})
                            for media_obj in content.values():
                                schema = media_obj.get('schema', {})
                                if self._has_detailed_error_schema(schema):
                                    detailed_error_responses += 1
                                    break
                            break
        
        if total_operations == 0:
            return 0
        
        error_coverage = (operations_with_errors / total_operations) * 60
        error_detail = (detailed_error_responses / operations_with_errors * 40) if operations_with_errors > 0 else 0
        
        return error_coverage + error_detail
    
    def _has_detailed_error_schema(self, schema: Dict[str, Any]) -> bool:
        """Check if error schema has detailed information."""
        if not isinstance(schema, dict):
            return False
        
        properties = schema.get('properties', {})
        error_fields = {'error', 'message', 'details', 'code', 'timestamp'}
        
        matching_fields = sum(1 for field in error_fields if field in properties)
        return matching_fields >= 2  # At least 2 standard error fields
    
    def _check_filtering_and_searching(self) -> float:
        """Check filtering and search capabilities."""
        paths = self.spec.get('paths', {})
        
        collection_endpoints = 0
        endpoints_with_filtering = 0
        
        for path, path_obj in paths.items():
            if not isinstance(path_obj, dict):
                continue
            
            # Identify collection endpoints (no path parameters)
            if '{' not in path:
                get_operation = path_obj.get('get')
                if get_operation:
                    collection_endpoints += 1
                    
                    # Check for filtering/search parameters
                    parameters = get_operation.get('parameters', [])
                    filter_params = {'filter', 'search', 'q', 'query', 'limit', 'page', 'offset'}
                    
                    has_filtering = any(
                        isinstance(param, dict) and param.get('name', '').lower() in filter_params
                        for param in parameters
                    )
                    
                    if has_filtering:
                        endpoints_with_filtering += 1
        
        return (endpoints_with_filtering / collection_endpoints * 100) if collection_endpoints > 0 else 100
    
    def _check_resource_relationships(self) -> float:
        """Check resource relationship handling."""
        paths = self.spec.get('paths', {})
        schemas = self.spec.get('components', {}).get('schemas', {})
        
        # Look for nested resource patterns
        nested_resources = 0
        total_resource_paths = 0
        
        for path in paths.keys():
            # Skip root paths
            if path.count('/') >= 3:  # e.g., /api/users/123/orders
                total_resource_paths += 1
                if '{' in path:
                    nested_resources += 1
        
        # Look for relationship fields in schemas
        schemas_with_relations = 0
        for schema in schemas.values():
            if not isinstance(schema, dict):
                continue
            
            properties = schema.get('properties', {})
            relation_indicators = {'id', '_id', 'ref', 'link', 'relationship'}
            
            has_relations = any(
                any(indicator in prop_name.lower() for indicator in relation_indicators)
                for prop_name in properties.keys()
            )
            
            if has_relations:
                schemas_with_relations += 1
        
        nested_score = (nested_resources / total_resource_paths * 50) if total_resource_paths > 0 else 25
        schema_score = (schemas_with_relations / len(schemas) * 50) if schemas else 25
        
        return nested_score + schema_score
    
    def _check_developer_experience(self) -> float:
        """Check overall developer experience factors."""
        # This is a composite score based on various DX factors
        factors = []
        
        # Factor 1: Consistent response structure
        factors.append(self._check_response_consistency())
        
        # Factor 2: Clear operation IDs
        paths = self.spec.get('paths', {})
        total_operations = 0
        operations_with_ids = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
                
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                
                total_operations += 1
                if isinstance(operation, dict) and operation.get('operationId'):
                    operations_with_ids += 1
        
        operation_id_score = (operations_with_ids / total_operations * 100) if total_operations > 0 else 100
        factors.append(operation_id_score)
        
        # Factor 3: Reasonable path complexity
        avg_path_complexity = 0
        if paths:
            complexities = []
            for path in paths.keys():
                segments = [seg for seg in path.split('/') if seg]
                complexities.append(len(segments))
            
            avg_complexity = sum(complexities) / len(complexities)
            # Optimal complexity is 3-4 segments
            if 3 <= avg_complexity <= 4:
                avg_path_complexity = 100
            elif 2 <= avg_complexity <= 5:
                avg_path_complexity = 80
            else:
                avg_path_complexity = 60
        
        factors.append(avg_path_complexity)
        
        return sum(factors) / len(factors) if factors else 0
    
    def _score_performance(self) -> None:
        """Score API performance patterns (15% weight)."""
        category = ScoreCategory.PERFORMANCE
        score = CategoryScore(
            category=category,
            score=0.0,
            max_score=100.0,
            weight=self.category_weights[category]
        )
        
        performance_checks = [
            self._check_caching_headers(),
            self._check_pagination_patterns(),
            self._check_compression_support(),
            self._check_efficiency_patterns(),
            self._check_batch_operations()
        ]
        
        valid_scores = [s for s in performance_checks if s is not None]
        if valid_scores:
            score.score = sum(valid_scores) / len(valid_scores)
        
        # Add recommendations
        if score.score < 60:
            score.recommendations.extend([
                "Implement pagination for list endpoints",
                "Add caching headers for cacheable responses",
                "Consider batch operations for bulk updates"
            ])
        elif score.score < 80:
            score.recommendations.extend([
                "Review caching strategies for better performance",
                "Consider field selection parameters for large responses"
            ])
        
        self.scorecard.category_scores[category] = score
    
    def _check_caching_headers(self) -> float:
        """Check caching header implementation."""
        paths = self.spec.get('paths', {})
        
        get_operations = 0
        cacheable_operations = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
            
            get_operation = path_obj.get('get')
            if get_operation and isinstance(get_operation, dict):
                get_operations += 1
                
                # Check for caching-related headers in responses
                responses = get_operation.get('responses', {})
                for response in responses.values():
                    if not isinstance(response, dict):
                        continue
                    
                    headers = response.get('headers', {})
                    cache_headers = {'cache-control', 'etag', 'last-modified', 'expires'}
                    
                    if any(header.lower() in cache_headers for header in headers.keys()):
                        cacheable_operations += 1
                        break
        
        return (cacheable_operations / get_operations * 100) if get_operations > 0 else 50
    
    def _check_pagination_patterns(self) -> float:
        """Check pagination implementation."""
        paths = self.spec.get('paths', {})
        
        collection_endpoints = 0
        paginated_endpoints = 0
        
        for path, path_obj in paths.items():
            if not isinstance(path_obj, dict):
                continue
            
            # Identify collection endpoints
            if '{' not in path:  # No path parameters = collection
                get_operation = path_obj.get('get')
                if get_operation and isinstance(get_operation, dict):
                    collection_endpoints += 1
                    
                    # Check for pagination parameters
                    parameters = get_operation.get('parameters', [])
                    pagination_params = {'limit', 'offset', 'page', 'pagesize', 'per_page', 'cursor'}
                    
                    has_pagination = any(
                        isinstance(param, dict) and param.get('name', '').lower() in pagination_params
                        for param in parameters
                    )
                    
                    if has_pagination:
                        paginated_endpoints += 1
        
        return (paginated_endpoints / collection_endpoints * 100) if collection_endpoints > 0 else 100
    
    def _check_compression_support(self) -> float:
        """Check compression support indicators."""
        # This is speculative - OpenAPI doesn't directly specify compression
        # Look for indicators that compression is considered
        
        servers = self.spec.get('servers', [])
        
        # Check if any server descriptions mention compression
        compression_mentions = 0
        for server in servers:
            if isinstance(server, dict):
                description = server.get('description', '').lower()
                if any(term in description for term in ['gzip', 'compress', 'deflate']):
                    compression_mentions += 1
        
        # Base score - assume compression is handled at server level
        base_score = 70
        
        if compression_mentions > 0:
            return min(base_score + (compression_mentions * 10), 100)
        
        return base_score
    
    def _check_efficiency_patterns(self) -> float:
        """Check efficiency patterns like field selection."""
        paths = self.spec.get('paths', {})
        
        total_get_operations = 0
        operations_with_selection = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
            
            get_operation = path_obj.get('get')
            if get_operation and isinstance(get_operation, dict):
                total_get_operations += 1
                
                # Check for field selection parameters
                parameters = get_operation.get('parameters', [])
                selection_params = {'fields', 'select', 'include', 'exclude'}
                
                has_selection = any(
                    isinstance(param, dict) and param.get('name', '').lower() in selection_params
                    for param in parameters
                )
                
                if has_selection:
                    operations_with_selection += 1
        
        return (operations_with_selection / total_get_operations * 100) if total_get_operations > 0 else 60
    
    def _check_batch_operations(self) -> float:
        """Check for batch operation support."""
        paths = self.spec.get('paths', {})
        
        # Look for batch endpoints
        batch_indicators = ['batch', 'bulk', 'multi']
        batch_endpoints = 0
        
        for path in paths.keys():
            if any(indicator in path.lower() for indicator in batch_indicators):
                batch_endpoints += 1
        
        # Look for array-based request bodies (indicating batch operations)
        array_operations = 0
        total_post_put_operations = 0
        
        for path_obj in paths.values():
            if not isinstance(path_obj, dict):
                continue
            
            for method in ['post', 'put', 'patch']:
                operation = path_obj.get(method)
                if operation and isinstance(operation, dict):
                    total_post_put_operations += 1
                    
                    request_body = operation.get('requestBody', {})
                    content = request_body.get('content', {})
                    
                    for media_obj in content.values():
                        schema = media_obj.get('schema', {})
                        if schema.get('type') == 'array':
                            array_operations += 1
                            break
        
        # Score based on presence of batch patterns
        batch_score = min(batch_endpoints * 20, 60)  # Up to 60 points for explicit batch endpoints
        
        if total_post_put_operations > 0:
            array_score = (array_operations / total_post_put_operations) * 40
            batch_score += array_score
        
        return min(batch_score, 100)
    
    def generate_json_report(self) -> str:
        """Generate JSON format scorecard."""
        report_data = {
            "overall": {
                "score": round(self.scorecard.overall_score, 2),
                "grade": self.scorecard.overall_grade,
                "totalEndpoints": self.scorecard.total_endpoints
            },
            "api_info": self.scorecard.api_info,
            "categories": {},
            "topRecommendations": self.scorecard.get_top_recommendations()
        }
        
        for category, score in self.scorecard.category_scores.items():
            report_data["categories"][category.value] = {
                "score": round(score.score, 2),
                "grade": score.letter_grade,
                "weight": score.weight,
                "weightedScore": round(score.weighted_score, 2),
                "issues": score.issues,
                "recommendations": score.recommendations
            }
        
        return json.dumps(report_data, indent=2)
    
    def generate_text_report(self) -> str:
        """Generate human-readable scorecard report."""
        lines = [
            "═══════════════════════════════════════════════════════════════",
            "                      API DESIGN SCORECARD",
            "═══════════════════════════════════════════════════════════════",
            f"API: {self.scorecard.api_info.get('title', 'Unknown')}",
            f"Version: {self.scorecard.api_info.get('version', 'Unknown')}",
            f"Total Endpoints: {self.scorecard.total_endpoints}",
            "",
            f"šŸ† OVERALL GRADE: {self.scorecard.overall_grade} ({self.scorecard.overall_score:.1f}/100.0)",
            "",
            "═══════════════════════════════════════════════════════════════",
            "DETAILED BREAKDOWN:",
            "═══════════════════════════════════════════════════════════════"
        ]
        
        # Sort categories by weight (most important first)
        sorted_categories = sorted(
            self.scorecard.category_scores.items(),
            key=lambda x: x[1].weight,
            reverse=True
        )
        
        for category, score in sorted_categories:
            category_name = category.value.title().replace('_', ' ')
            
            lines.extend([
                "",
                f"šŸ“Š {category_name.upper()} - Grade: {score.letter_grade} ({score.score:.1f}/100)",
                f"   Weight: {score.weight}% | Contribution: {score.weighted_score:.1f} points",
                "   " + "─" * 50
            ])
            
            if score.recommendations:
                lines.append("   šŸ’” Recommendations:")
                for rec in score.recommendations[:3]:  # Top 3 recommendations
                    lines.append(f"      • {rec}")
            else:
                lines.append("   āœ… No specific recommendations - performing well!")
        
        # Overall assessment
        lines.extend([
            "",
            "═══════════════════════════════════════════════════════════════",
            "OVERALL ASSESSMENT:",
            "═══════════════════════════════════════════════════════════════"
        ])
        
        if self.scorecard.overall_grade == "A":
            lines.extend([
                "šŸ† EXCELLENT! Your API demonstrates outstanding design quality.",
                "   Continue following these best practices and consider sharing",
                "   your approach as a reference for other teams."
            ])
        elif self.scorecard.overall_grade == "B":
            lines.extend([
                "āœ… GOOD! Your API follows most best practices with room for",
                "   minor improvements. Focus on the recommendations above",
                "   to achieve excellence."
            ])
        elif self.scorecard.overall_grade == "C":
            lines.extend([
                "āš ļø  FAIR! Your API has a solid foundation but several areas",
                "   need improvement. Prioritize the high-weight categories",
                "   for maximum impact."
            ])
        elif self.scorecard.overall_grade == "D":
            lines.extend([
                "āŒ NEEDS IMPROVEMENT! Your API has significant issues that",
                "   may impact developer experience and maintainability.",
                "   Focus on consistency and documentation first."
            ])
        else:  # Grade F
            lines.extend([
                "🚨 CRITICAL ISSUES! Your API requires major redesign to meet",
                "   basic quality standards. Consider comprehensive review",
                "   of design principles and best practices."
            ])
        
        # Top recommendations
        top_recs = self.scorecard.get_top_recommendations(3)
        if top_recs:
            lines.extend([
                "",
                "šŸŽÆ TOP PRIORITY RECOMMENDATIONS:",
                ""
            ])
            for i, rec in enumerate(top_recs, 1):
                lines.append(f"   {i}. {rec}")
        
        lines.extend([
            "",
            "═══════════════════════════════════════════════════════════════",
            f"Generated by API Scorecard Tool | Score: {self.scorecard.overall_grade} ({self.scorecard.overall_score:.1f}%)",
            "═══════════════════════════════════════════════════════════════"
        ])
        
        return "\n".join(lines)


def main():
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        description="Generate comprehensive API design quality scorecard",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python api_scorecard.py openapi.json
  python api_scorecard.py --format json openapi.json > scorecard.json
  python api_scorecard.py --output scorecard.txt openapi.json
        """
    )
    
    parser.add_argument(
        'spec_file',
        help='OpenAPI/Swagger specification file (JSON format)'
    )
    
    parser.add_argument(
        '--format',
        choices=['text', 'json'],
        default='text',
        help='Output format (default: text)'
    )
    
    parser.add_argument(
        '--output',
        help='Output file (default: stdout)'
    )
    
    parser.add_argument(
        '--min-grade',
        choices=['A', 'B', 'C', 'D', 'F'],
        help='Exit with code 1 if grade is below minimum'
    )
    
    args = parser.parse_args()
    
    # Load specification file
    try:
        with open(args.spec_file, 'r') as f:
            spec = json.load(f)
    except FileNotFoundError:
        print(f"Error: Specification file '{args.spec_file}' not found.", file=sys.stderr)
        return 1
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in '{args.spec_file}': {e}", file=sys.stderr)
        return 1
    
    # Initialize scoring engine and generate scorecard
    engine = APIScoringEngine()
    
    try:
        scorecard = engine.score_api(spec)
    except Exception as e:
        print(f"Error during scoring: {e}", file=sys.stderr)
        return 1
    
    # Generate report
    if args.format == 'json':
        output = engine.generate_json_report()
    else:
        output = engine.generate_text_report()
    
    # Write output
    if args.output:
        try:
            with open(args.output, 'w') as f:
                f.write(output)
            print(f"Scorecard written to {args.output}")
        except IOError as e:
            print(f"Error writing to '{args.output}': {e}", file=sys.stderr)
            return 1
    else:
        print(output)
    
    # Check minimum grade requirement
    if args.min_grade:
        grade_order = ['F', 'D', 'C', 'B', 'A']
        current_grade_index = grade_order.index(scorecard.overall_grade)
        min_grade_index = grade_order.index(args.min_grade)
        
        if current_grade_index < min_grade_index:
            print(f"Grade {scorecard.overall_grade} is below minimum required grade {args.min_grade}", file=sys.stderr)
            return 1
    
    return 0


if __name__ == '__main__':
    sys.exit(main())

Skill Companion Files

Additional files collected from the skill directory layout.

references/api_antipatterns.md

# Common API Anti-Patterns and How to Avoid Them

## Introduction

This document outlines common anti-patterns in REST API design that can lead to poor developer experience, maintenance nightmares, and scalability issues. Each anti-pattern is accompanied by examples and recommended solutions.

## 1. Verb-Based URLs (The RPC Trap)

### Anti-Pattern
Using verbs in URLs instead of treating endpoints as resources.

āŒ Bad Examples: POST /api/getUsers POST /api/createUser GET /api/deleteUser/123 POST /api/updateUserPassword GET /api/calculateOrderTotal/456


### Why It's Bad
- Violates REST principles
- Makes the API feel like RPC instead of REST
- HTTP methods lose their semantic meaning
- Reduces cacheability
- Harder to understand resource relationships

### Solution

āœ… Good Examples: GET /api/users # Get users POST /api/users # Create user DELETE /api/users/123 # Delete user PATCH /api/users/123/password # Update password GET /api/orders/456/total # Get order total


## 2. Inconsistent Naming Conventions

### Anti-Pattern
Mixed naming conventions across the API.

```json
āŒ Bad Examples:
{
  "user_id": 123,           // snake_case
  "firstName": "John",      // camelCase
  "last-name": "Doe",       // kebab-case
  "EMAIL": "john@example.com", // UPPER_CASE
  "IsActive": true          // PascalCase
}

Why It's Bad

  • Confuses developers
  • Increases cognitive load
  • Makes code generation difficult
  • Reduces API adoption

Solution

āœ… Choose one convention and stick to it (camelCase recommended):
{
  "userId": 123,
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@example.com",
  "isActive": true
}

3. Ignoring HTTP Status Codes

Anti-Pattern

Always returning HTTP 200 regardless of the actual result.

āŒ Bad Example:
HTTP/1.1 200 OK
{
  "status": "error",
  "code": 404,
  "message": "User not found"
}

Why It's Bad

  • Breaks HTTP semantics
  • Prevents proper error handling by clients
  • Breaks caching and proxies
  • Makes monitoring and debugging harder

Solution

āœ… Good Example:
HTTP/1.1 404 Not Found
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User with ID 123 not found",
    "requestId": "req-abc123"
  }
}

4. Overly Complex Nested Resources

Anti-Pattern

Creating deeply nested URL structures that are hard to navigate.

āŒ Bad Example:
/companies/123/departments/456/teams/789/members/012/projects/345/tasks/678/comments/901

Why It's Bad

  • URLs become unwieldy
  • Creates tight coupling between resources
  • Makes independent resource access difficult
  • Complicates authorization logic

Solution

āœ… Good Examples:
/tasks/678                    # Direct access to task
/tasks/678/comments          # Task comments
/users/012/tasks             # User's tasks
/projects/345?team=789       # Project filtering

5. Inconsistent Error Response Formats

Anti-Pattern

Different error response structures across endpoints.

āŒ Bad Examples:
# Endpoint 1
{"error": "Invalid email"}

# Endpoint 2  
{"success": false, "msg": "User not found", "code": 404}

# Endpoint 3
{"errors": [{"field": "name", "message": "Required"}]}

Why It's Bad

  • Makes error handling complex for clients
  • Reduces code reusability
  • Poor developer experience

Solution

āœ… Standardized Error Format:
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid data",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Email address is not valid"
      }
    ],
    "requestId": "req-123456",
    "timestamp": "2024-02-16T13:00:00Z"
  }
}

6. Missing or Poor Pagination

Anti-Pattern

Returning all results in a single response or inconsistent pagination.

āŒ Bad Examples:
# No pagination (returns 10,000 records)
GET /api/users

# Inconsistent pagination parameters
GET /api/users?page=1&size=10
GET /api/orders?offset=0&limit=20
GET /api/products?start=0&count=50

Why It's Bad

  • Can cause performance issues
  • May overwhelm clients
  • Inconsistent pagination parameters confuse developers
  • No way to estimate total results

Solution

āœ… Good Example:
GET /api/users?page=1&pageSize=10

{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 10,
    "total": 150,
    "totalPages": 15,
    "hasNext": true,
    "hasPrev": false
  }
}

7. Exposing Internal Implementation Details

Anti-Pattern

URLs and field names that reflect database structure or internal architecture.

āŒ Bad Examples:
/api/user_table/123
/api/db_orders
/api/legacy_customer_data
/api/temp_migration_users

Response fields:
{
  "user_id_pk": 123,
  "internal_ref_code": "usr_abc",
  "db_created_timestamp": 1645123456
}

Why It's Bad

  • Couples API to internal implementation
  • Makes refactoring difficult
  • Exposes unnecessary technical details
  • Reduces API longevity

Solution

āœ… Good Examples:
/api/users/123
/api/orders
/api/customers

Response fields:
{
  "id": 123,
  "referenceCode": "usr_abc",
  "createdAt": "2024-02-16T13:00:00Z"
}

8. Overloading Single Endpoint

Anti-Pattern

Using one endpoint for multiple unrelated operations based on request parameters.

āŒ Bad Example:
POST /api/user-actions
{
  "action": "create_user",
  "userData": {...}
}

POST /api/user-actions  
{
  "action": "delete_user",
  "userId": 123
}

POST /api/user-actions
{
  "action": "send_email",
  "userId": 123,
  "emailType": "welcome"
}

Why It's Bad

  • Breaks REST principles
  • Makes documentation complex
  • Complicates client implementation
  • Reduces discoverability

Solution

āœ… Good Examples:
POST   /api/users              # Create user
DELETE /api/users/123         # Delete user  
POST   /api/users/123/emails   # Send email to user

9. Lack of Versioning Strategy

Anti-Pattern

Making breaking changes without version management.

āŒ Bad Examples:
# Original API
{
  "name": "John Doe",
  "age": 30
}

# Later (breaking change with no versioning)
{
  "firstName": "John",
  "lastName": "Doe", 
  "birthDate": "1994-02-16"
}

Why It's Bad

  • Breaks existing clients
  • Forces all clients to update simultaneously
  • No graceful migration path
  • Reduces API stability

Solution

āœ… Good Examples:
# Version 1
GET /api/v1/users/123
{
  "name": "John Doe",
  "age": 30
}

# Version 2 (with both versions supported)
GET /api/v2/users/123
{
  "firstName": "John",
  "lastName": "Doe",
  "birthDate": "1994-02-16",
  "age": 30  // Backwards compatibility
}

10. Poor Error Messages

Anti-Pattern

Vague, unhelpful, or technical error messages.

āŒ Bad Examples:
{"error": "Something went wrong"}
{"error": "Invalid input"}
{"error": "SQL constraint violation: FK_user_profile_id"}
{"error": "NullPointerException at line 247"}

Why It's Bad

  • Doesn't help developers fix issues
  • Increases support burden
  • Poor developer experience
  • May expose sensitive information

Solution

āœ… Good Examples:
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The email address is required and must be in a valid format",
    "details": [
      {
        "field": "email",
        "code": "REQUIRED",
        "message": "Email address is required"
      }
    ]
  }
}

11. Ignoring Content Negotiation

Anti-Pattern

Hard-coding response format without considering client preferences.

āŒ Bad Example:
# Always returns JSON regardless of Accept header
GET /api/users/123
Accept: application/xml
# Returns JSON anyway

Why It's Bad

  • Reduces API flexibility
  • Ignores HTTP standards
  • Makes integration harder for diverse clients

Solution

āœ… Good Example:
GET /api/users/123
Accept: application/xml

HTTP/1.1 200 OK
Content-Type: application/xml

<?xml version="1.0"?>
<user>
  <id>123</id>
  <name>John Doe</name>
</user>

12. Stateful API Design

Anti-Pattern

Maintaining session state on the server between requests.

āŒ Bad Example:
# Step 1: Initialize session
POST /api/session/init

# Step 2: Set context (requires step 1)
POST /api/session/set-user/123

# Step 3: Get data (requires steps 1 & 2)
GET /api/session/user-data

Why It's Bad

  • Breaks REST statelessness principle
  • Reduces scalability
  • Makes caching difficult
  • Complicates error recovery

Solution

āœ… Good Example:
# Self-contained requests
GET /api/users/123/data
Authorization: Bearer jwt-token-with-context

13. Inconsistent HTTP Method Usage

Anti-Pattern

Using HTTP methods inappropriately or inconsistently.

āŒ Bad Examples:
GET  /api/users/123/delete    # DELETE operation with GET
POST /api/users/123/get       # GET operation with POST
PUT  /api/users               # Creating with PUT on collection
GET  /api/users/search        # Search with side effects

Why It's Bad

  • Violates HTTP semantics
  • Breaks caching and idempotency expectations
  • Confuses developers and tools

Solution

āœ… Good Examples:
DELETE /api/users/123         # Delete with DELETE
GET    /api/users/123         # Get with GET
POST   /api/users             # Create on collection
GET    /api/users?q=search    # Safe search with GET

14. Missing Rate Limiting Information

Anti-Pattern

Not providing rate limiting information to clients.

āŒ Bad Example:
HTTP/1.1 429 Too Many Requests
{
  "error": "Rate limit exceeded"
}

Why It's Bad

  • Clients don't know when to retry
  • No information about current limits
  • Difficult to implement proper backoff strategies

Solution

āœ… Good Example:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 3600

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "API rate limit exceeded",
    "retryAfter": 3600
  }
}

15. Chatty API Design

Anti-Pattern

Requiring multiple API calls to accomplish common tasks.

āŒ Bad Example:
# Get user profile requires 4 API calls
GET /api/users/123           # Basic info
GET /api/users/123/profile   # Profile details
GET /api/users/123/settings  # User settings
GET /api/users/123/stats     # User statistics

Why It's Bad

  • Increases latency
  • Creates network overhead
  • Makes mobile apps inefficient
  • Complicates client implementation

Solution

āœ… Good Examples:
# Single call with expansion
GET /api/users/123?include=profile,settings,stats

# Or provide composite endpoints
GET /api/users/123/dashboard

# Or batch operations
POST /api/batch
{
  "requests": [
    {"method": "GET", "url": "/users/123"},
    {"method": "GET", "url": "/users/123/profile"}
  ]
}

16. No Input Validation

Anti-Pattern

Accepting and processing invalid input without proper validation.

āŒ Bad Example:
POST /api/users
{
  "email": "not-an-email",
  "age": -5,
  "name": ""
}

# API processes this and fails later or stores invalid data

Why It's Bad

  • Leads to data corruption
  • Security vulnerabilities
  • Difficult to debug issues
  • Poor user experience

Solution

āœ… Good Example:
POST /api/users
{
  "email": "not-an-email",
  "age": -5,
  "name": ""
}

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid data",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Email must be a valid email address"
      },
      {
        "field": "age",
        "code": "INVALID_RANGE",
        "message": "Age must be between 0 and 150"
      },
      {
        "field": "name",
        "code": "REQUIRED",
        "message": "Name is required and cannot be empty"
      }
    ]
  }
}

17. Synchronous Long-Running Operations

Anti-Pattern

Blocking the client with long-running operations in synchronous endpoints.

āŒ Bad Example:
POST /api/reports/generate
# Client waits 30 seconds for response

Why It's Bad

  • Poor user experience
  • Timeouts and connection issues
  • Resource waste on client and server
  • Doesn't scale well

Solution

āœ… Good Example:
# Async pattern
POST /api/reports
HTTP/1.1 202 Accepted
Location: /api/reports/job-123
{
  "jobId": "job-123",
  "status": "processing",
  "estimatedCompletion": "2024-02-16T13:05:00Z"
}

# Check status
GET /api/reports/job-123
{
  "jobId": "job-123",
  "status": "completed",
  "result": "/api/reports/download/report-456"
}

Prevention Strategies

1. API Design Reviews

  • Implement mandatory design reviews
  • Use checklists based on these anti-patterns
  • Include multiple stakeholders

2. API Style Guides

  • Create and enforce API style guides
  • Use linting tools for consistency
  • Regular training for development teams

3. Automated Testing

  • Test for common anti-patterns
  • Include contract testing
  • Monitor API usage patterns

4. Documentation Standards

  • Require comprehensive API documentation
  • Include examples and error scenarios
  • Keep documentation up-to-date

5. Client Feedback

  • Regularly collect feedback from API consumers
  • Monitor API usage analytics
  • Conduct developer experience surveys

Conclusion

Avoiding these anti-patterns requires:

  • Understanding REST principles
  • Consistent design standards
  • Regular review and refactoring
  • Focus on developer experience
  • Proper tooling and automation

Remember: A well-designed API is an asset that grows in value over time, while a poorly designed API becomes a liability that hampers development and adoption.


### references/rest_design_rules.md

```markdown
# REST API Design Rules Reference

## Core Principles

### 1. Resources, Not Actions
REST APIs should focus on **resources** (nouns) rather than **actions** (verbs). The HTTP methods provide the actions.

āœ… Good: GET /users # Get all users GET /users/123 # Get user 123 POST /users # Create new user PUT /users/123 # Update user 123 DELETE /users/123 # Delete user 123

āŒ Bad: POST /getUsers POST /createUser POST /updateUser/123 POST /deleteUser/123


### 2. Hierarchical Resource Structure
Use hierarchical URLs to represent resource relationships:

/users/123/orders/456/items/789


But avoid excessive nesting (max 3-4 levels):

āŒ Too deep: /companies/123/departments/456/teams/789/members/012/tasks/345 āœ… Better: /tasks/345?member=012&team=789


## Resource Naming Conventions

### URLs Should Use Kebab-Case

āœ… Good: /user-profiles /order-items /shipping-addresses

āŒ Bad: /userProfiles /user_profiles /orderItems


### Collections vs Individual Resources

Collection: /users Individual: /users/123 Sub-resource: /users/123/orders


### Pluralization Rules
- Use **plural nouns** for collections: `/users`, `/orders`
- Use **singular nouns** for single resources: `/user-profile`, `/current-session`
- Be consistent throughout your API

## HTTP Methods Usage

### GET - Safe and Idempotent
- **Purpose**: Retrieve data
- **Safe**: No side effects
- **Idempotent**: Multiple calls return same result
- **Request Body**: Should not have one
- **Cacheable**: Yes

GET /users/123 GET /users?status=active&limit=10


### POST - Not Idempotent
- **Purpose**: Create resources, non-idempotent operations
- **Safe**: No
- **Idempotent**: No
- **Request Body**: Usually required
- **Cacheable**: Generally no

POST /users # Create new user POST /users/123/activate # Activate user (action)


### PUT - Idempotent
- **Purpose**: Create or completely replace a resource
- **Safe**: No
- **Idempotent**: Yes
- **Request Body**: Required (complete resource)
- **Cacheable**: No

PUT /users/123 # Replace entire user resource


### PATCH - Partial Update
- **Purpose**: Partially update a resource
- **Safe**: No
- **Idempotent**: Not necessarily
- **Request Body**: Required (partial resource)
- **Cacheable**: No

PATCH /users/123 # Update only specified fields


### DELETE - Idempotent
- **Purpose**: Remove a resource
- **Safe**: No
- **Idempotent**: Yes (same result if called multiple times)
- **Request Body**: Usually not needed
- **Cacheable**: No

DELETE /users/123


## Status Codes

### Success Codes (2xx)
- **200 OK**: Standard success response
- **201 Created**: Resource created successfully (POST)
- **202 Accepted**: Request accepted for processing (async)
- **204 No Content**: Success with no response body (DELETE, PUT)

### Redirection Codes (3xx)
- **301 Moved Permanently**: Resource permanently moved
- **302 Found**: Temporary redirect
- **304 Not Modified**: Use cached version

### Client Error Codes (4xx)
- **400 Bad Request**: Invalid request syntax or data
- **401 Unauthorized**: Authentication required
- **403 Forbidden**: Access denied (user authenticated but not authorized)
- **404 Not Found**: Resource not found
- **405 Method Not Allowed**: HTTP method not supported
- **409 Conflict**: Resource conflict (duplicates, version mismatch)
- **422 Unprocessable Entity**: Valid syntax but semantic errors
- **429 Too Many Requests**: Rate limit exceeded

### Server Error Codes (5xx)
- **500 Internal Server Error**: Unexpected server error
- **502 Bad Gateway**: Invalid response from upstream server
- **503 Service Unavailable**: Server temporarily unavailable
- **504 Gateway Timeout**: Upstream server timeout

## URL Design Patterns

### Query Parameters for Filtering

GET /users?status=active GET /users?role=admin&department=engineering GET /orders?created_after=2024-01-01&status=pending


### Pagination Parameters

Offset-based

GET /users?offset=20&limit=10

Cursor-based

GET /users?cursor=eyJpZCI6MTIzfQ&limit=10

Page-based

GET /users?page=3&page_size=10


### Sorting Parameters

GET /users?sort=created_at # Ascending GET /users?sort=-created_at # Descending (prefix with -) GET /users?sort=last_name,first_name # Multiple fields


### Field Selection

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


### Search Parameters

GET /users?q=john GET /products?search=laptop&category=electronics


## Response Format Standards

### Consistent Response Structure
```json
{
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "meta": {
    "timestamp": "2024-02-16T13:00:00Z",
    "version": "1.0"
  }
}

Collection Responses

{
  "data": [
    {"id": 1, "name": "Item 1"},
    {"id": 2, "name": "Item 2"}
  ],
  "pagination": {
    "total": 150,
    "page": 1,
    "pageSize": 10,
    "totalPages": 15,
    "hasNext": true,
    "hasPrev": false
  },
  "meta": {
    "timestamp": "2024-02-16T13:00:00Z"
  }
}

Error Response Format

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid parameters",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Email address is not valid"
      }
    ],
    "requestId": "req-123456",
    "timestamp": "2024-02-16T13:00:00Z"
  }
}

Field Naming Conventions

Use camelCase for JSON Fields

āœ… Good:
{
  "firstName": "John",
  "lastName": "Doe",
  "createdAt": "2024-02-16T13:00:00Z",
  "isActive": true
}

āŒ Bad:
{
  "first_name": "John",
  "LastName": "Doe",
  "created-at": "2024-02-16T13:00:00Z"
}

Boolean Fields

Use positive, clear names with "is", "has", "can", or "should" prefixes:

āœ… Good:
{
  "isActive": true,
  "hasPermission": false,
  "canEdit": true,
  "shouldNotify": false
}

āŒ Bad:
{
  "active": true,
  "disabled": false,  // Double negative
  "permission": false // Unclear meaning
}

Date/Time Fields

  • Use ISO 8601 format: 2024-02-16T13:00:00Z
  • Include timezone information
  • Use consistent field naming:
{
  "createdAt": "2024-02-16T13:00:00Z",
  "updatedAt": "2024-02-16T13:30:00Z",
  "deletedAt": null,
  "publishedAt": "2024-02-16T14:00:00Z"
}

Content Negotiation

Accept Headers

Accept: application/json
Accept: application/xml
Accept: application/json; version=1

Content-Type Headers

Content-Type: application/json
Content-Type: application/json; charset=utf-8
Content-Type: multipart/form-data

Versioning via Headers

Accept: application/vnd.myapi.v1+json
API-Version: 1.0

Caching Guidelines

Cache-Control Headers

Cache-Control: public, max-age=3600        # Cache for 1 hour
Cache-Control: private, max-age=0          # Don't cache
Cache-Control: no-cache, must-revalidate   # Always validate

ETags for Conditional Requests

HTTP/1.1 200 OK
ETag: "123456789"
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

# Client subsequent request:
If-None-Match: "123456789"
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

Security Headers

Authentication

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Basic dXNlcjpwYXNzd29yZA==
Authorization: Api-Key abc123def456

CORS Headers

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

Rate Limiting

Rate Limit Headers

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

Rate Limit Exceeded Response

HTTP/1.1 429 Too Many Requests
Retry-After: 3600

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "API rate limit exceeded",
    "details": {
      "limit": 1000,
      "window": "1 hour",
      "retryAfter": 3600
    }
  }
}

Hypermedia (HATEOAS)

Links in Responses

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "_links": {
    "self": {
      "href": "/users/123"
    },
    "orders": {
      "href": "/users/123/orders"
    },
    "edit": {
      "href": "/users/123",
      "method": "PUT"
    },
    "delete": {
      "href": "/users/123",
      "method": "DELETE"
    }
  }
}

Link Relations

  • self: Link to the resource itself
  • edit: Link to edit the resource
  • delete: Link to delete the resource
  • related: Link to related resources
  • next/prev: Pagination links

Common Anti-Patterns to Avoid

1. Verbs in URLs

āŒ Bad: /api/getUser/123
āœ… Good: GET /api/users/123

2. Inconsistent Naming

āŒ Bad: /user-profiles and /userAddresses
āœ… Good: /user-profiles and /user-addresses

3. Deep Nesting

āŒ Bad: /companies/123/departments/456/teams/789/members/012
āœ… Good: /team-members/012?team=789

4. Ignoring HTTP Status Codes

āŒ Bad: Always return 200 with error info in body
āœ… Good: Use appropriate status codes (404, 400, 500, etc.)

5. Exposing Internal Structure

āŒ Bad: /api/database_table_users
āœ… Good: /api/users

6. No Versioning Strategy

āŒ Bad: Breaking changes without version management
āœ… Good: /api/v1/users or Accept: application/vnd.api+json;version=1

7. Inconsistent Error Responses

āŒ Bad: Different error formats for different endpoints
āœ… Good: Standardized error response structure

Best Practices Summary

  1. Use nouns for resources, not verbs
  2. Leverage HTTP methods correctly
  3. Maintain consistent naming conventions
  4. Implement proper error handling
  5. Use appropriate HTTP status codes
  6. Design for cacheability
  7. Implement security from the start
  8. Plan for versioning
  9. Provide comprehensive documentation
  10. Follow HATEOAS principles when applicable

Further Reading

Source: https://github.com/alirezarezvani/claude-skills#engineering-api-design-reviewer

Content curated from original sources, copyright belongs to authors

Grade B
-AI Score
Best Practices
Checking...
Try this Skill

User Rating

USER RATING

0UP
0DOWN
Loading files...

WORKS WITH

Claude Code
Claude
Codex CLI
Codex
Gemini CLI
Gemini
O
OpenCode
O
OpenClaw
GitHub Copilot
Copilot
Cursor
Cursor
W
Windsurf
C
Cline
R
Roo
K
Kiro
J
Junie
A
Augment
W
Warp
G
Goose