add brain
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/)
|
||||
@@ -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())
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user