
Api Design Reviewer
- 147 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Audit REST or GraphQL API designs for naming consistency, versioning, error models, pagination, auth patterns, and consumer ergonomics before implementation or public release.
About
API design reviewer skill from borghei/claude-skills that audits proposed REST or GraphQL interfaces for naming, versioning, pagination, authentication, error handling, and DX consistency, catching contract risks before APIs ship to consumers.
- REST and GraphQL contract critique
- Versioning, pagination, and error model checks
- Auth and naming consistency review
- Developer-experience focused recommendations
Api Design Reviewer by the numbers
- 147 all-time installs (skills.sh)
- Ranked #2,555 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill api-design-reviewerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 147 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Audit REST or GraphQL API designs for naming consistency, versioning, error models, pagination, auth patterns, and consumer ergonomics before implementation or public release.
Files
API Design Reviewer
Comprehensive analysis and review of REST API designs against conventions, best practices, and industry standards. Helps engineering teams build consistent, maintainable, well-designed APIs through automated linting, breaking-change detection, and design scorecards.
Core Capabilities
- API linting & convention analysis — resource naming (kebab-case URLs, camelCase fields), HTTP method usage, URL structure, status-code compliance, error-format consistency, and documentation coverage.
- Breaking change detection — endpoint removal, response-shape changes, field removal/rename, type changes, new required fields, and status-code changes between two spec versions, with migration guides.
- API design scoring — weighted scorecard across Consistency (30%), Documentation (20%), Security (20%), Usability (15%), and Performance (15%), with letter grades A–F and recommendations.
When to Use
- Designing a new REST API or reviewing an API contract.
- Validating an OpenAPI/Swagger spec against REST conventions.
- Managing API versioning and detecting breaking changes between releases.
- Gating deployments on API design quality in CI.
Clarify First
Before producing the review, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- [ ] Which output — lint report, design scorecard, or breaking-change detection (selects
api_linter.py,api_scorecard.py, orbreaking_change_detector.py) - [ ] Spec file(s) — the OpenAPI/Swagger JSON, or the two versions to diff (the input the tools parse)
- [ ] Quality bar / CI gate — minimum grade or fail-on-breaking (sets
--min-grade/--exit-on-breakingand how strict the verdict is)
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Tools
| Tool | Purpose | Command |
|---|---|---|
api_linter.py | Lint an OpenAPI/Swagger JSON spec for REST conventions and best practices | python scripts/api_linter.py openapi.json --format json |
breaking_change_detector.py | Detect breaking changes between two spec versions (with migration guides) | python scripts/breaking_change_detector.py v1.json v2.json --exit-on-breaking |
api_scorecard.py | Score API design quality across 5 weighted dimensions (A–F grades) | python scripts/api_scorecard.py openapi.json --min-grade B |
References
Load the reference that matches the task — keep this file lean and pull detail on demand:
- [references/rest-design-patterns.md](references/rest-design-patterns.md) — REST naming/method/URL principles, versioning strategies, pagination patterns, error formats and status codes, auth/RBAC patterns, rate limiting, HATEOAS, idempotency, backward-compatibility rules, OpenAPI validation, performance, and security best practices. Read when designing or reviewing endpoints.
- [references/tooling-ci-and-troubleshooting.md](references/tooling-ci-and-troubleshooting.md) — the three tools' features, CI/CD and pre-commit integration, best-practices and anti-pattern checklists, troubleshooting table, success criteria, and full CLI flag references. Read when wiring tools into pipelines or debugging.
- [references/rest_design_rules.md](references/rest_design_rules.md) — detailed REST design rules reference (resources vs actions, HTTP method semantics with worked examples). Read for an in-depth rules catalog.
- [references/api_antipatterns.md](references/api_antipatterns.md) — common API anti-patterns (verb-based URLs / RPC trap and more) with bad/good examples and recommended fixes. Read when auditing an existing API for design smells.
Scope & Limitations
This skill covers:
- Linting OpenAPI 3.x and Swagger 2.0 JSON specifications against REST conventions
- Detecting breaking, potentially-breaking, and non-breaking changes between two spec versions
- Scoring API design quality across consistency, documentation, security, usability, and performance
- Generating actionable migration guides when breaking changes are found
This skill does NOT cover:
- Runtime API testing, load testing, or contract testing (see
api-test-suite-builder) - GraphQL, gRPC, or WebSocket API design review
- Auto-generation of OpenAPI specs from code or server stubs
- Authentication flow implementation or OAuth server configuration (see
senior-securityin engineering/)
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
engineering/api-test-suite-builder | Generate test cases from linter findings | Linter issues feed into test plan priorities for endpoint validation |
engineering/changelog-generator | Document breaking changes in release notes | Breaking change detector output provides structured change data for changelogs |
engineering/ci-cd-pipeline-builder | Gate deployments on API quality | Scorecard grade and linter exit codes integrate as pipeline quality gates |
engineering/senior-backend | Review API implementation against design | Scorecard recommendations guide backend refactoring decisions |
engineering/code-reviewer | Enrich PR reviews with API analysis | Linter and breaking change reports attach to PR review comments |
engineering/release-manager | Validate version bumps match change severity | Breaking change detector severity levels inform semver version decisions |
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/456Why 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 total2. Inconsistent Naming Conventions
Anti-Pattern
Mixed naming conventions across the API.
❌ Bad Examples:
{
"user_id": 123, // snake_case
"firstName": "John", // camelCase
"last-name": "Doe", // kebab-case
"EMAIL": "john@example.com", // UPPER_CASE
"IsActive": true // PascalCase
}Why It's Bad
- Confuses developers
- Increases cognitive load
- Makes code generation difficult
- Reduces API adoption
Solution
✅ Choose one convention and stick to it (camelCase recommended):
{
"userId": 123,
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"isActive": true
}3. Ignoring HTTP Status Codes
Anti-Pattern
Always returning HTTP 200 regardless of the actual result.
❌ Bad Example:
HTTP/1.1 200 OK
{
"status": "error",
"code": 404,
"message": "User not found"
}Why It's Bad
- Breaks HTTP semantics
- Prevents proper error handling by clients
- Breaks caching and proxies
- Makes monitoring and debugging harder
Solution
✅ Good Example:
HTTP/1.1 404 Not Found
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID 123 not found",
"requestId": "req-abc123"
}
}4. Overly Complex Nested Resources
Anti-Pattern
Creating deeply nested URL structures that are hard to navigate.
❌ Bad Example:
/companies/123/departments/456/teams/789/members/012/projects/345/tasks/678/comments/901Why 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 filtering5. Inconsistent Error Response Formats
Anti-Pattern
Different error response structures across endpoints.
❌ Bad Examples:
# Endpoint 1
{"error": "Invalid email"}
# Endpoint 2
{"success": false, "msg": "User not found", "code": 404}
# Endpoint 3
{"errors": [{"field": "name", "message": "Required"}]}Why It's Bad
- Makes error handling complex for clients
- Reduces code reusability
- Poor developer experience
Solution
✅ Standardized Error Format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email address is not valid"
}
],
"requestId": "req-123456",
"timestamp": "2024-02-16T13:00:00Z"
}
}6. Missing or Poor Pagination
Anti-Pattern
Returning all results in a single response or inconsistent pagination.
❌ Bad Examples:
# No pagination (returns 10,000 records)
GET /api/users
# Inconsistent pagination parameters
GET /api/users?page=1&size=10
GET /api/orders?offset=0&limit=20
GET /api/products?start=0&count=50Why It's Bad
- Can cause performance issues
- May overwhelm clients
- Inconsistent pagination parameters confuse developers
- No way to estimate total results
Solution
✅ Good Example:
GET /api/users?page=1&pageSize=10
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 10,
"total": 150,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
}
}7. Exposing Internal Implementation Details
Anti-Pattern
URLs and field names that reflect database structure or internal architecture.
❌ Bad Examples:
/api/user_table/123
/api/db_orders
/api/legacy_customer_data
/api/temp_migration_users
Response fields:
{
"user_id_pk": 123,
"internal_ref_code": "usr_abc",
"db_created_timestamp": 1645123456
}Why It's Bad
- Couples API to internal implementation
- Makes refactoring difficult
- Exposes unnecessary technical details
- Reduces API longevity
Solution
✅ Good Examples:
/api/users/123
/api/orders
/api/customers
Response fields:
{
"id": 123,
"referenceCode": "usr_abc",
"createdAt": "2024-02-16T13:00:00Z"
}8. Overloading Single Endpoint
Anti-Pattern
Using one endpoint for multiple unrelated operations based on request parameters.
❌ Bad Example:
POST /api/user-actions
{
"action": "create_user",
"userData": {...}
}
POST /api/user-actions
{
"action": "delete_user",
"userId": 123
}
POST /api/user-actions
{
"action": "send_email",
"userId": 123,
"emailType": "welcome"
}Why It's Bad
- Breaks REST principles
- Makes documentation complex
- Complicates client implementation
- Reduces discoverability
Solution
✅ Good Examples:
POST /api/users # Create user
DELETE /api/users/123 # Delete user
POST /api/users/123/emails # Send email to user9. Lack of Versioning Strategy
Anti-Pattern
Making breaking changes without version management.
❌ Bad Examples:
# Original API
{
"name": "John Doe",
"age": 30
}
# Later (breaking change with no versioning)
{
"firstName": "John",
"lastName": "Doe",
"birthDate": "1994-02-16"
}Why It's Bad
- Breaks existing clients
- Forces all clients to update simultaneously
- No graceful migration path
- Reduces API stability
Solution
✅ Good Examples:
# Version 1
GET /api/v1/users/123
{
"name": "John Doe",
"age": 30
}
# Version 2 (with both versions supported)
GET /api/v2/users/123
{
"firstName": "John",
"lastName": "Doe",
"birthDate": "1994-02-16",
"age": 30 // Backwards compatibility
}10. Poor Error Messages
Anti-Pattern
Vague, unhelpful, or technical error messages.
❌ Bad Examples:
{"error": "Something went wrong"}
{"error": "Invalid input"}
{"error": "SQL constraint violation: FK_user_profile_id"}
{"error": "NullPointerException at line 247"}Why It's Bad
- Doesn't help developers fix issues
- Increases support burden
- Poor developer experience
- May expose sensitive information
Solution
✅ Good Examples:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The email address is required and must be in a valid format",
"details": [
{
"field": "email",
"code": "REQUIRED",
"message": "Email address is required"
}
]
}
}11. Ignoring Content Negotiation
Anti-Pattern
Hard-coding response format without considering client preferences.
❌ Bad Example:
# Always returns JSON regardless of Accept header
GET /api/users/123
Accept: application/xml
# Returns JSON anywayWhy 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-dataWhy 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-context13. 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 effectsWhy 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 GET14. 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 statisticsWhy It's Bad
- Increases latency
- Creates network overhead
- Makes mobile apps inefficient
- Complicates client implementation
Solution
✅ Good Examples:
# Single call with expansion
GET /api/users/123?include=profile,settings,stats
# Or provide composite endpoints
GET /api/users/123/dashboard
# Or batch operations
POST /api/batch
{
"requests": [
{"method": "GET", "url": "/users/123"},
{"method": "GET", "url": "/users/123/profile"}
]
}16. No Input Validation
Anti-Pattern
Accepting and processing invalid input without proper validation.
❌ Bad Example:
POST /api/users
{
"email": "not-an-email",
"age": -5,
"name": ""
}
# API processes this and fails later or stores invalid dataWhy It's Bad
- Leads to data corruption
- Security vulnerabilities
- Difficult to debug issues
- Poor user experience
Solution
✅ Good Example:
POST /api/users
{
"email": "not-an-email",
"age": -5,
"name": ""
}
HTTP/1.1 400 Bad Request
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email must be a valid email address"
},
{
"field": "age",
"code": "INVALID_RANGE",
"message": "Age must be between 0 and 150"
},
{
"field": "name",
"code": "REQUIRED",
"message": "Name is required and cannot be empty"
}
]
}
}17. Synchronous Long-Running Operations
Anti-Pattern
Blocking the client with long-running operations in synchronous endpoints.
❌ Bad Example:
POST /api/reports/generate
# Client waits 30 seconds for responseWhy 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.
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/1232. Hierarchical Resource Structure
Use hierarchical URLs to represent resource relationships:
/users/123/orders/456/items/789But 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=789Resource Naming Conventions
URLs Should Use Kebab-Case
✅ Good:
/user-profiles
/order-items
/shipping-addresses
❌ Bad:
/userProfiles
/user_profiles
/orderItemsCollections vs Individual Resources
Collection: /users
Individual: /users/123
Sub-resource: /users/123/ordersPluralization 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=10POST - 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 resourcePATCH - 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 fieldsDELETE - Idempotent
- Purpose: Remove a resource
- Safe: No
- Idempotent: Yes (same result if called multiple times)
- Request Body: Usually not needed
- Cacheable: No
DELETE /users/123Status 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=pendingPagination 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=10Sorting Parameters
GET /users?sort=created_at # Ascending
GET /users?sort=-created_at # Descending (prefix with -)
GET /users?sort=last_name,first_name # Multiple fieldsField Selection
GET /users?fields=id,name,email
GET /users/123?include=orders,profile
GET /users/123?exclude=internal_notesSearch Parameters
GET /users?q=john
GET /products?search=laptop&category=electronicsResponse Format Standards
Consistent Response Structure
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"meta": {
"timestamp": "2024-02-16T13:00:00Z",
"version": "1.0"
}
}Collection Responses
{
"data": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"}
],
"pagination": {
"total": 150,
"page": 1,
"pageSize": 10,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
},
"meta": {
"timestamp": "2024-02-16T13:00:00Z"
}
}Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid parameters",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email address is not valid"
}
],
"requestId": "req-123456",
"timestamp": "2024-02-16T13:00:00Z"
}
}Field Naming Conventions
Use camelCase for JSON Fields
✅ Good:
{
"firstName": "John",
"lastName": "Doe",
"createdAt": "2024-02-16T13:00:00Z",
"isActive": true
}
❌ Bad:
{
"first_name": "John",
"LastName": "Doe",
"created-at": "2024-02-16T13:00:00Z"
}Boolean Fields
Use positive, clear names with "is", "has", "can", or "should" prefixes:
✅ Good:
{
"isActive": true,
"hasPermission": false,
"canEdit": true,
"shouldNotify": false
}
❌ Bad:
{
"active": true,
"disabled": false, // Double negative
"permission": false // Unclear meaning
}Date/Time Fields
- Use ISO 8601 format:
2024-02-16T13:00:00Z - Include timezone information
- Use consistent field naming:
{
"createdAt": "2024-02-16T13:00:00Z",
"updatedAt": "2024-02-16T13:30:00Z",
"deletedAt": null,
"publishedAt": "2024-02-16T14:00:00Z"
}Content Negotiation
Accept Headers
Accept: application/json
Accept: application/xml
Accept: application/json; version=1Content-Type Headers
Content-Type: application/json
Content-Type: application/json; charset=utf-8
Content-Type: multipart/form-dataVersioning via Headers
Accept: application/vnd.myapi.v1+json
API-Version: 1.0Caching 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 validateETags 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 GMTSecurity Headers
Authentication
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Basic dXNlcjpwYXNzd29yZA==
Authorization: Api-Key abc123def456CORS Headers
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, AuthorizationRate Limiting
Rate Limit Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200
X-RateLimit-Window: 3600Rate Limit Exceeded Response
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "API rate limit exceeded",
"details": {
"limit": 1000,
"window": "1 hour",
"retryAfter": 3600
}
}
}Hypermedia (HATEOAS)
Links in Responses
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": {
"href": "/users/123"
},
"orders": {
"href": "/users/123/orders"
},
"edit": {
"href": "/users/123",
"method": "PUT"
},
"delete": {
"href": "/users/123",
"method": "DELETE"
}
}
}Link Relations
- self: Link to the resource itself
- edit: Link to edit the resource
- delete: Link to delete the resource
- related: Link to related resources
- next/prev: Pagination links
Common Anti-Patterns to Avoid
1. Verbs in URLs
❌ Bad: /api/getUser/123
✅ Good: GET /api/users/1232. Inconsistent Naming
❌ Bad: /user-profiles and /userAddresses
✅ Good: /user-profiles and /user-addresses3. Deep Nesting
❌ Bad: /companies/123/departments/456/teams/789/members/012
✅ Good: /team-members/012?team=7894. 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/users6. No Versioning Strategy
❌ Bad: Breaking changes without version management
✅ Good: /api/v1/users or Accept: application/vnd.api+json;version=17. Inconsistent Error Responses
❌ Bad: Different error formats for different endpoints
✅ Good: Standardized error response structureBest 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
REST API Design Patterns
Read this when reviewing or designing endpoints — naming, versioning, pagination, error formats, auth, rate limiting, HATEOAS, idempotency, backward compatibility, OpenAPI validation, performance, and security.
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/lineItemsHTTP 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=adminVersioning Strategies
1. URL Versioning (Recommended)
/api/v1/users
/api/v2/usersPros: Clear, explicit, easy to route Cons: URL proliferation, caching complexity
2. Header Versioning
GET /api/users
Accept: application/vnd.api+json;version=1Pros: Clean URLs, content negotiation Cons: Less visible, harder to test manually
3. Media Type Versioning
GET /api/users
Accept: application/vnd.myapi.v1+jsonPros: RESTful, supports multiple representations Cons: Complex, harder to implement
4. Query Parameter Versioning
/api/users?version=1Pros: Simple to implement Cons: Not RESTful, can be ignored
Pagination Patterns
Offset-Based Pagination
{
"data": [...],
"pagination": {
"offset": 20,
"limit": 10,
"total": 150,
"hasMore": true
}
}Cursor-Based Pagination
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIzfQ==",
"hasMore": true
}
}Page-Based Pagination
{
"data": [...],
"pagination": {
"page": 3,
"pageSize": 10,
"totalPages": 15,
"totalItems": 150
}
}Error Response Formats
Standard Error Structure
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid parameters",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email address is not valid"
}
],
"requestId": "req-123456",
"timestamp": "2024-02-16T13:00:00Z"
}
}HTTP Status Code Usage
- 400 Bad Request: Invalid request syntax or parameters
- 401 Unauthorized: Authentication required
- 403 Forbidden: Access denied (authenticated but not authorized)
- 404 Not Found: Resource not found
- 409 Conflict: Resource conflict (duplicate, version mismatch)
- 422 Unprocessable Entity: Valid syntax but semantic errors
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Server Error: Unexpected server error
Authentication and Authorization Patterns
Bearer Token Authentication
Authorization: Bearer <token>API Key Authentication
X-API-Key: <api-key>
Authorization: Api-Key <api-key>OAuth 2.0 Flow
Authorization: Bearer <oauth-access-token>Role-Based Access Control (RBAC)
{
"user": {
"id": "123",
"roles": ["admin", "editor"],
"permissions": ["read:users", "write:orders"]
}
}Rate Limiting Implementation
Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200Response on Limit Exceeded
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"retryAfter": 3600
}
}HATEOAS (Hypermedia as the Engine of Application State)
Example Implementation
{
"id": "123",
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": { "href": "/api/v1/users/123" },
"orders": { "href": "/api/v1/users/123/orders" },
"profile": { "href": "/api/v1/users/123/profile" },
"deactivate": {
"href": "/api/v1/users/123/deactivate",
"method": "POST"
}
}
}Idempotency
Idempotent Methods
- GET: Always safe and idempotent
- PUT: Should be idempotent (replace entire resource)
- DELETE: Should be idempotent (same result)
- PATCH: May or may not be idempotent
Idempotency Keys
POST /api/v1/payments
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000Backward 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 GMTEfficient 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
Tooling, CI Integration, Best Practices, Troubleshooting & Tool Reference
Read this when wiring the tools into CI/pre-commit, applying the best-practice/anti-pattern checklists, troubleshooting, or needing full CLI flag references.
Tools and Scripts
api_linter.py
Analyzes API specifications for compliance with REST conventions and best practices.
Features:
- OpenAPI/Swagger spec validation
- Naming convention checks
- HTTP method usage validation
- Error format consistency
- Documentation completeness analysis
breaking_change_detector.py
Compares API specification versions to identify breaking changes.
Features:
- Endpoint comparison
- Schema change detection
- Field removal/modification tracking
- Migration guide generation
- Impact severity assessment
api_scorecard.py
Provides comprehensive scoring of API design quality.
Features:
- Multi-dimensional scoring
- Detailed improvement recommendations
- Letter grade assessment (A-F)
- Benchmark comparisons
- Progress tracking
Integration Examples
CI/CD Integration
- name: API Linting
run: python scripts/api_linter.py openapi.json
- name: Breaking Change Detection
run: python scripts/breaking_change_detector.py openapi-v1.json openapi-v2.json
- name: API Scorecard
run: python scripts/api_scorecard.py openapi.jsonPre-commit Hooks
#!/bin/bash
python engineering/api-design-reviewer/scripts/api_linter.py api/openapi.json
if [ $? -ne 0 ]; then
echo "API linting failed. Please fix the issues before committing."
exit 1
fiBest 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
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Linter reports false positives on action endpoints (e.g., /activate) | Verb detection flags action segments as REST anti-patterns | Action endpoints are acceptable for non-CRUD operations; suppress with caution or restructure as POST /resource/{id}/activations |
| Breaking change detector misses schema-level changes | Input specs lack components/schemas definitions or use inline schemas | Ensure both old and new specs define reusable schemas under components/schemas for accurate comparison |
| Scorecard gives low security score despite auth being implemented | Security schemes are defined but not applied globally or per-operation | Add a top-level security array in the OpenAPI spec and reference securitySchemes under components |
| Linter exits with code 1 on specs with no endpoints | Zero endpoints with any structural error triggers a non-zero exit | Verify the spec contains at least one path under paths; the linter requires endpoints to produce a meaningful score |
| JSON parse error on valid YAML OpenAPI specs | All three tools accept JSON input only | Convert YAML specs to JSON before running tools: python -c "import yaml,json,sys; json.dump(yaml.safe_load(open(sys.argv[1])),open(sys.argv[2],'w'),indent=2)" spec.yaml spec.json |
| Naming convention warnings on legacy APIs with snake_case fields | Linter enforces camelCase for properties and kebab-case for URL segments | For brownfield APIs, address naming in new endpoints first and plan a migration for existing fields across a major version bump |
| Scorecard reports 0% for performance category | Spec contains no caching headers, pagination, or compression references | Add Cache-Control response headers, define pagination query parameters (limit, offset or cursor), and document compression support |
Success Criteria
- Zero breaking changes detected between consecutive minor or patch releases (semver compliance)
- API consistency score (from
api_scorecard.py) above 90 across all reviewed specifications - Overall scorecard grade of B or higher (80+) before any API ships to production
- 100% of endpoints include at least one success response and one error response definition
- All path segments follow kebab-case naming and all schema properties follow camelCase naming with zero linter errors
- Breaking change reports generated and reviewed for every PR that modifies an OpenAPI spec
- Documentation coverage score above 85%, meaning every operation has a summary and every schema has a description
Tool Reference
api_linter.py
Purpose: Analyzes OpenAPI/Swagger JSON specifications for compliance with REST conventions and best practices. Checks naming conventions, HTTP method usage, URL structure, status codes, error formats, documentation completeness, and security configuration.
Usage:
python api_linter.py [OPTIONS] INPUT_FILEParameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
input_file | positional | Yes | -- | Path to OpenAPI/Swagger JSON file or raw endpoints JSON |
--format | option | No | text | Output format: text or json |
--raw-endpoints | flag | No | off | Treat input as raw endpoint definitions instead of an OpenAPI spec |
--output | option | No | stdout | Write report to the specified file path |
Example:
python api_linter.py openapi.json
python api_linter.py --format json openapi.json > report.json
python api_linter.py --raw-endpoints endpoints.json
python api_linter.py --output lint-report.txt openapi.jsonOutput Formats:
- text -- Human-readable report with issue breakdown by category, severity icons, suggestions, and a scoring summary. Exits with code 1 if any errors are found, 0 otherwise.
- json -- Machine-readable JSON object with
summary(total_endpoints, endpoints_with_issues, total_issues, errors, warnings, info, score) andissuesarray (severity, category, message, path, suggestion).
---
breaking_change_detector.py
Purpose: Compares two versions of an OpenAPI JSON specification and detects breaking changes including removed endpoints, modified response structures, removed fields, type changes, new required fields, parameter changes, and status code changes. Generates migration guides for each breaking change.
Usage:
python breaking_change_detector.py [OPTIONS] OLD_SPEC NEW_SPECParameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
old_spec | positional | Yes | -- | Path to the old (baseline) API specification JSON file |
new_spec | positional | Yes | -- | Path to the new API specification JSON file |
--format | option | No | text | Output format: text or json |
--output | option | No | stdout | Write report to the specified file path |
--exit-on-breaking | flag | No | off | Exit with code 1 if any breaking changes are detected |
Example:
python breaking_change_detector.py v1.json v2.json
python breaking_change_detector.py --format json v1.json v2.json > changes.json
python breaking_change_detector.py --exit-on-breaking --output report.txt v1.json v2.jsonOutput Formats:
- text -- Human-readable report listing each change with its type (breaking, potentially_breaking, non_breaking, enhancement), severity (critical, high, medium, low, info), category, path, message, impact description, and migration guide.
- json -- Machine-readable JSON object with
summary(total_changes, breaking_changes, potentially_breaking_changes, non_breaking_changes, enhancements, and per-severity counts) andchangesarray (changeType, severity, category, path, message, oldValue, newValue, migrationGuide, impactDescription).
---
api_scorecard.py
Purpose: Generates a comprehensive API design quality scorecard by evaluating an OpenAPI JSON specification across five weighted dimensions: Consistency (30%), Documentation (20%), Security (20%), Usability (15%), and Performance (15%). Produces letter grades (A-F) per category and overall, with actionable improvement recommendations.
Usage:
python api_scorecard.py [OPTIONS] SPEC_FILEParameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
spec_file | positional | Yes | -- | Path to the OpenAPI/Swagger specification JSON file |
--format | option | No | text | Output format: text or json |
--output | option | No | stdout | Write scorecard to the specified file path |
--min-grade | option | No | none | Minimum acceptable grade (A, B, C, D, F); exits with code 1 if the overall grade falls below this threshold |
Example:
python api_scorecard.py openapi.json
python api_scorecard.py --format json openapi.json > scorecard.json
python api_scorecard.py --min-grade B --output scorecard.txt openapi.jsonOutput Formats:
- text -- Human-readable scorecard showing API info, per-category scores with letter grades, issue counts, recommendations, and an overall weighted score with grade.
- json -- Machine-readable JSON object with
apiInfo,categoryScores(per category: score, maxScore, weight, letterGrade, weightedScore, issues, recommendations),overallScore,overallGrade,totalEndpoints, andtopRecommendations.
#!/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())