API Design Expert

VerifiedSafe

Expert-level guidance on API design: REST principles, GraphQL, versioning strategies, authentication, and best practices for scalable APIs.

Sby Skills Guide Bot
DevelopmentIntermediate
3/11/2026
Claude Code
#api-design#rest#graphql#api-versioning#api-security

Recommended for

Automated summary

This skill provides principles and examples for designing REST and GraphQL APIs, focusing on resource structure, versioning, and best practices.

Strengths

  • Covers REST fundamentals (resources, statelessness, cacheability, uniform interface) and compares several API styles.
  • Shows a FastAPI example with Pydantic validation, pagination, status codes, and built-in documentation.
  • Highlights commonly missed concerns such as versioning, authentication, rate limiting, and error handling.

Limitations

  • The content appears truncated in the middle of the FastAPI example, so the route implementation is incomplete.
  • GraphQL, gRPC, WebSocket, and SSE are named but not explained with schemas or detailed practices.
  • Versioning and security strategies are listed as considerations but not explored in depth.
When to use it

Use it to shape or review the design of a REST API, especially route conventions, pagination, and error responses.

When not to use it

Avoid it if you need a complete operational guide for GraphQL, gRPC, or a production-ready FastAPI implementation.

Security analysis

Safe
Quality score78/100

The skill is a pure documentation/instruction file with illustrative FastAPI snippets and no shell, network, or filesystem-destructive instructions. The only concerns are pedagogical: placeholder credentials and simplified auth/rate-limiting examples that a careless reader might copy into production. No prompt-injection or exfiltration attempt was found in the content.

Findings
  • •Example JWT code uses a hardcoded placeholder secret (SECRET_KEY = "your-secret-key") and a fixed HS256 algorithm; it is clearly a placeholder, but readers could copy it verbatim into production.
  • •Example API-key check compares against a module-level `valid_api_keys` collection with `not in` (not constant-time) and no key rotation/expiry story; this is illustrative code, not an active exploit.
  • •The rate limiter is described as a 'token bucket algorithm' but is implemented as a fixed-window counter — an accuracy/security-hardening gap in the sample, not a runtime risk.
  • •No execution risk: the skill only declares Read/Write/Edit, and all code blocks are static FastAPI documentation with stub bodies (`pass`, `return []`, `raise HTTPException`).
  • •Example security section uses a hardcoded placeholder JWT signing key (literal string) — a reader copying the snippet verbatim would ship a predictable secret; should reference an environment variable instead
  • •Rate-limiting example constructs a Redis client from an undefined `redis_client` and stores client identity by raw IP only, which is a weak abuse-prevention example
  • •`verify_api_key` checks against an undefined `valid_api_keys` collection, and `get_user_from_db` is undefined — illustrative only, but misleading if copied
How this analysis is made

Examples

Design a users API
Design a versioned REST API for user management with pagination, filtering, validation, and consistent error responses. Use FastAPI-style examples.
Review REST conventions
Review these API endpoints for resource naming, HTTP methods, status codes, and pagination, and suggest a more consistent REST design.
Choose a versioning strategy
Compare URL, header, and media-type versioning for a public REST API and recommend one for backward compatibility.

name: api-design-expert version: 1.0.0 description: Expert-level API design principles, REST, GraphQL, versioning, and API best practices category: api tags: [api-design, rest, graphql, api-versioning, api-security] allowed-tools:

  • Read
  • Write
  • Edit

API Design Expert

Expert guidance for API design, RESTful principles, GraphQL, versioning strategies, and API best practices.

Core Concepts

API Design Principles

  • RESTful architecture
  • Resource-oriented design
  • Uniform interface
  • Statelessness
  • Cacheability
  • Layered system

API Styles

  • REST (Representational State Transfer)
  • GraphQL
  • RPC (Remote Procedure Call)
  • WebSocket
  • Server-Sent Events (SSE)
  • gRPC

Key Considerations

  • Versioning strategies
  • Authentication and authorization
  • Rate limiting
  • Error handling
  • Documentation
  • Backward compatibility

REST API Design

from fastapi import FastAPI, HTTPException, Query, Path, Header
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
from enum import Enum

app = FastAPI(
    title="User Management API",
    version="1.0.0",
    description="RESTful API for user management"
)

# Models
class UserRole(str, Enum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

class UserCreate(BaseModel):
    email: str = Field(..., example="user@example.com")
    name: str = Field(..., min_length=1, max_length=100)
    role: UserRole = UserRole.USER

class UserResponse(BaseModel):
    id: str
    email: str
    name: str
    role: UserRole
    created_at: datetime
    updated_at: datetime

    class Config:
        schema_extra = {
            "example": {
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "email": "user@example.com",
                "name": "John Doe",
                "role": "user",
                "created_at": "2024-01-01T00:00:00Z",
                "updated_at": "2024-01-01T00:00:00Z"
            }
        }

class UserUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    role: Optional[UserRole] = None

# REST Endpoints following best practices
@app.get("/api/v1/users",
         response_model=List[UserResponse],
         summary="List all users",
         tags=["Users"])
async def list_users(
    page: int = Query(1, ge=1, description="Page number"),
    page_size: int = Query(20, ge=1, le=100, description="Items per page"),
    sort: str = Query("created_at", description="Sort field"),
    order: str = Query("desc", regex="^(asc|desc)$")
):
    """
    Retrieve a paginated list of users.

    - **page**: Page number (starts at 1)
    - **page_size**: Number of items per page (1-100)
    - **sort**: Field to sort by
    - **order**: Sort order (asc or desc)
    """
    # Implementation
    return []

@app.get("/api/v1/users/{user_id}",
         response_model=UserResponse,
         summary="Get user by ID",
         tags=["Users"])
async def get_user(
    user_id: str = Path(..., description="User ID")
):
    """Retrieve a specific user by ID."""
    # Implementation
    raise HTTPException(status_code=404, detail="User not found")

@app.post("/api/v1/users",
          response_model=UserResponse,
          status_code=201,
          summary="Create new user",
          tags=["Users"])
async def create_user(user: UserCreate):
    """Create a new user."""
    # Implementation
    return UserResponse(
        id="123e4567-e89b-12d3-a456-426614174000",
        email=user.email,
        name=user.name,
        role=user.role,
        created_at=datetime.now(),
        updated_at=datetime.now()
    )

@app.patch("/api/v1/users/{user_id}",
           response_model=UserResponse,
           summary="Update user",
           tags=["Users"])
async def update_user(
    user_id: str = Path(..., description="User ID"),
    user: UserUpdate = None
):
    """Partially update a user."""
    # Implementation
    pass

@app.delete("/api/v1/users/{user_id}",
            status_code=204,
            summary="Delete user",
            tags=["Users"])
async def delete_user(
    user_id: str = Path(..., description="User ID")
):
    """Delete a user."""
    # Implementation
    pass

# Nested resources
@app.get("/api/v1/users/{user_id}/posts",
         summary="Get user posts",
         tags=["Users", "Posts"])
async def get_user_posts(user_id: str):
    """Retrieve all posts for a specific user."""
    return []

API Versioning

from fastapi import APIRouter, Request

# URL Path Versioning (Recommended)
v1_router = APIRouter(prefix="/api/v1")
v2_router = APIRouter(prefix="/api/v2")

@v1_router.get("/users")
async def get_users_v1():
    """Version 1: Returns basic user info"""
    return [{"id": 1, "name": "John"}]

@v2_router.get("/users")
async def get_users_v2():
    """Version 2: Returns enhanced user info"""
    return [{"id": 1, "name": "John", "email": "john@example.com"}]

# Header Versioning
async def version_from_header(request: Request):
    version = request.headers.get("API-Version", "1")
    return version

@app.get("/api/users")
async def get_users(version: str = Depends(version_from_header)):
    if version == "2":
        return get_users_v2()
    return get_users_v1()

# Content Negotiation Versioning
@app.get("/api/users")
async def get_users_content_negotiation(
    accept: str = Header("application/vnd.api+json; version=1")
):
    if "version=2" in accept:
        return get_users_v2()
    return get_users_v1()

Error Handling

from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ValidationError

class ErrorResponse(BaseModel):
    error: str
    message: str
    details: Optional[dict] = None
    timestamp: datetime
    path: str

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content=ErrorResponse(
            error=exc.status_code,
            message=exc.detail,
            timestamp=datetime.now(),
            path=request.url.path
        ).dict()
    )

@app.exception_handler(ValidationError)
async def validation_exception_handler(request: Request, exc: ValidationError):
    return JSONResponse(
        status_code=422,
        content=ErrorResponse(
            error="validation_error",
            message="Request validation failed",
            details=exc.errors(),
            timestamp=datetime.now(),
            path=request.url.path
        ).dict()
    )

# Custom business logic errors
class BusinessError(Exception):
    def __init__(self, message: str, code: str):
        self.message = message
        self.code = code

@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
    return JSONResponse(
        status_code=400,
        content=ErrorResponse(
            error=exc.code,
            message=exc.message,
            timestamp=datetime.now(),
            path=request.url.path
        ).dict()
    )

Rate Limiting

from fastapi import Request, HTTPException
from datetime import datetime, timedelta
import redis
from typing import Dict

class RateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    async def check_rate_limit(self,
                               key: str,
                               max_requests: int,
                               window_seconds: int) -> Dict:
        """
        Token bucket algorithm for rate limiting
        """
        now = datetime.now().timestamp()
        window_key = f"rate_limit:{key}:{int(now // window_seconds)}"

        pipe = self.redis.pipeline()
        pipe.incr(window_key)
        pipe.expire(window_key, window_seconds)
        result = pipe.execute()

        request_count = result[0]

        if request_count > max_requests:
            reset_time = (int(now // window_seconds) + 1) * window_seconds
            raise HTTPException(
                status_code=429,
                detail="Rate limit exceeded",
                headers={
                    "X-RateLimit-Limit": str(max_requests),
                    "X-RateLimit-Remaining": "0",
                    "X-RateLimit-Reset": str(reset_time)
                }
            )

        return {
            "limit": max_requests,
            "remaining": max_requests - request_count,
            "reset": (int(now // window_seconds) + 1) * window_seconds
        }

# Middleware for rate limiting
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    limiter = RateLimiter(redis_client)

    # Get client identifier (IP, API key, etc.)
    client_id = request.client.host

    try:
        rate_info = await limiter.check_rate_limit(
            client_id,
            max_requests=100,
            window_seconds=60
        )

        response = await call_next(request)

        # Add rate limit headers
        response.headers["X-RateLimit-Limit"] = str(rate_info["limit"])
        response.headers["X-RateLimit-Remaining"] = str(rate_info["remaining"])
        response.headers["X-RateLimit-Reset"] = str(rate_info["reset"])

        return response
    except HTTPException as e:
        return JSONResponse(
            status_code=e.status_code,
            content={"error": e.detail},
            headers=e.headers
        )

HATEOAS Implementation

from typing import Dict, List

class HATEOASResponse(BaseModel):
    data: dict
    links: Dict[str, str]

def create_links(resource_id: str, resource_type: str) -> Dict[str, str]:
    """Create HATEOAS links for a resource"""
    return {
        "self": f"/api/v1/{resource_type}/{resource_id}",
        "update": f"/api/v1/{resource_type}/{resource_id}",
        "delete": f"/api/v1/{resource_type}/{resource_id}",
        "collection": f"/api/v1/{resource_type}"
    }

@app.get("/api/v1/users/{user_id}", response_model=HATEOASResponse)
async def get_user_with_links(user_id: str):
    user = get_user_from_db(user_id)

    return HATEOASResponse(
        data=user,
        links={
            "self": f"/api/v1/users/{user_id}",
            "posts": f"/api/v1/users/{user_id}/posts",
            "profile": f"/api/v1/users/{user_id}/profile",
            "update": f"/api/v1/users/{user_id}",
            "delete": f"/api/v1/users/{user_id}"
        }
    )

API Security

from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext

security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"

async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    """Verify JWT token"""
    try:
        payload = jwt.decode(
            credentials.credentials,
            SECRET_KEY,
            algorithms=[ALGORITHM]
        )
        return payload
    except JWTError:
        raise HTTPException(
            status_code=401,
            detail="Invalid authentication credentials"
        )

@app.get("/api/v1/protected")
async def protected_route(token_payload: dict = Depends(verify_token)):
    return {"message": "Access granted", "user": token_payload}

# API Key Authentication
async def verify_api_key(api_key: str = Header(...)):
    """Verify API key"""
    if api_key not in valid_api_keys:
        raise HTTPException(status_code=403, detail="Invalid API key")
    return api_key

Best Practices

Design

  • Use nouns for resources, not verbs
  • Use plural names for collections
  • Use HTTP methods correctly (GET, POST, PUT, PATCH, DELETE)
  • Return appropriate status codes
  • Support filtering, sorting, and pagination
  • Use consistent naming conventions
  • Version APIs from the start

Documentation

  • Use OpenAPI/Swagger
  • Provide example requests and responses
  • Document error codes and messages
  • Include authentication requirements
  • Keep documentation up-to-date
  • Provide SDKs when possible

Performance

  • Implement caching (ETags, Cache-Control)
  • Support compression (gzip)
  • Paginate large result sets
  • Use async operations for long tasks
  • Implement rate limiting
  • Monitor API performance

Security

  • Use HTTPS always
  • Implement authentication and authorization
  • Validate all inputs
  • Rate limit to prevent abuse
  • Log security events
  • Use API keys or OAuth 2.0
  • Implement CORS properly

Anti-Patterns

❌ Using GET for state-changing operations ❌ Returning inconsistent response formats ❌ No versioning strategy ❌ Poor error messages ❌ No rate limiting ❌ Exposing internal implementation details ❌ Breaking changes without versioning

Resources

  • REST API Design Best Practices: https://restfulapi.net/
  • OpenAPI Specification: https://swagger.io/specification/
  • API Design Guide: https://cloud.google.com/apis/design
  • Microsoft REST API Guidelines: https://github.com/microsoft/api-guidelines
  • FastAPI: https://fastapi.tiangolo.com/

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/api-design-expert-mmlj2o5i/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/api-design-expert-mmlj2o5i' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/api-design-expert-mmlj2o5i/download' --output '.skill-downloads/api-design-expert-mmlj2o5i/SKILL.md.part' && mv '.skill-downloads/api-design-expert-mmlj2o5i/SKILL.md.part' '.skill-downloads/api-design-expert-mmlj2o5i/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.

Claude Code
test -f '.skill-downloads/api-design-expert-mmlj2o5i/SKILL.md' && test -s '.skill-downloads/api-design-expert-mmlj2o5i/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/api-design-expert' && cp -R '.skill-downloads/api-design-expert-mmlj2o5i' '.claude/skills/api-design-expert'

OpenSkills

OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.

Official OpenSkills installation
openskills install './.skill-downloads/api-design-expert-mmlj2o5i'

openskills read 'api-design-expert-mmlj2o5i'

Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.

For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.

openskills sync

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/api-design-expert-mmlj2o5i/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills