add brain

This commit is contained in:
2026-03-12 15:17:52 +07:00
parent fd9f558fa1
commit e7821a7a9d
355 changed files with 93784 additions and 24 deletions

View File

@@ -0,0 +1,421 @@
---
name: "api-design-reviewer"
description: "API Design Reviewer"
---
# 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
```json
{
"data": [...],
"pagination": {
"offset": 20,
"limit": 10,
"total": 150,
"hasMore": true
}
}
```
### Cursor-Based Pagination
```json
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIzfQ==",
"hasMore": true
}
}
```
### Page-Based Pagination
```json
{
"data": [...],
"pagination": {
"page": 3,
"pageSize": 10,
"totalPages": 15,
"totalItems": 150
}
}
```
## Error Response Formats
### Standard Error Structure
```json
{
"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)
```json
{
"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
```json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"retryAfter": 3600
}
}
```
## HATEOAS (Hypermedia as the Engine of Application State)
### Example Implementation
```json
{
"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
```yaml
- 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
```bash
#!/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.

View File

@@ -0,0 +1,680 @@
# 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
```json
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.
```json
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
```json
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.
```json
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
```json
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.
```json
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
```json
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.
```json
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
```json
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.
```json
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
```json
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.

View File

@@ -0,0 +1,487 @@
# 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
```json
{
"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
```json
{
"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
```json
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:
```json
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:
```json
{
"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
```json
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
```json
{
"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
- [RFC 7231 - HTTP/1.1 Semantics and Content](https://tools.ietf.org/html/rfc7231)
- [RFC 6570 - URI Template](https://tools.ietf.org/html/rfc6570)
- [OpenAPI Specification](https://swagger.io/specification/)
- [REST API Design Best Practices](https://www.restapitutorial.com/)
- [HTTP Status Code Definitions](https://httpstatuses.com/)

View File

@@ -0,0 +1,914 @@
#!/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())