Source profileQuality 96/100

affaan-m/ECC/skills/api-design/SKILL.md

api-design

REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs.

Source repository stars
234,327
Declared platforms
1
Static risk flags
0
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

Conventions and best practices for designing consistent, developer-friendly REST APIs.

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorDeclaredSource recordInstall path and trigger
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "skills/api-design"
    Safe inspection promptEditorial

    Inspect the Agent Skill "api-design" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/skills/api-design/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      Implementation

      SELECT FROM users ORDER BY createdat DESC LIMIT 20 OFFSET 20;

      SELECT FROM users ORDER BY createdat DESC LIMIT 20 OFFSET 20;GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
    2. 02

      Implementation Patterns

      Review the “Implementation Patterns” section in the pinned source before continuing.

      Review and apply the “Implementation Patterns” source section.
    3. 03

      When to Activate

      Designing new API endpoints

      Designing new API endpointsReviewing existing API contractsAdding pagination, filtering, or sorting
    4. 04

      Resource Design

      Review the “Resource Design” section in the pinned source before continuing.

      Review and apply the “Resource Design” source section.
    5. 05

      URL Structure

      Review the “URL Structure” section in the pinned source before continuing.

      Review and apply the “URL Structure” source section.

    Permission review

    Static risk signals and limitations

    No configured static risk pattern was detected

    This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score96/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility1 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    skills/api-design/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    API Design Patterns

    Conventions and best practices for designing consistent, developer-friendly REST APIs.

    When to Activate

    • Designing new API endpoints
    • Reviewing existing API contracts
    • Adding pagination, filtering, or sorting
    • Implementing error handling for APIs
    • Planning API versioning strategy
    • Building public or partner-facing APIs

    Resource Design

    URL Structure

    # Resources are nouns, plural, lowercase, kebab-case
    GET    /api/v1/users
    GET    /api/v1/users/:id
    POST   /api/v1/users
    PUT    /api/v1/users/:id
    PATCH  /api/v1/users/:id
    DELETE /api/v1/users/:id
    
    # Sub-resources for relationships
    GET    /api/v1/users/:id/orders
    POST   /api/v1/users/:id/orders
    
    # Actions that don't map to CRUD (use verbs sparingly)
    POST   /api/v1/orders/:id/cancel
    POST   /api/v1/auth/login
    POST   /api/v1/auth/refresh
    

    Naming Rules

    # GOOD
    /api/v1/team-members          # kebab-case for multi-word resources
    /api/v1/orders?status=active  # query params for filtering
    /api/v1/users/123/orders      # nested resources for ownership
    
    # BAD
    /api/v1/getUsers              # verb in URL
    /api/v1/user                  # singular (use plural)
    /api/v1/team_members          # snake_case in URLs
    /api/v1/users/123/getOrders   # verb in nested resource
    

    HTTP Methods and Status Codes

    Method Semantics

    MethodIdempotentSafeUse For
    GETYesYesRetrieve resources
    POSTNoNoCreate resources, trigger actions
    PUTYesNoFull replacement of a resource
    PATCHNo*NoPartial update of a resource
    DELETEYesNoRemove a resource

    *PATCH can be made idempotent with proper implementation

    Status Code Reference

    # Success
    200 OK                    — GET, PUT, PATCH (with response body)
    201 Created               — POST (include Location header)
    204 No Content            — DELETE, PUT (no response body)
    
    # Client Errors
    400 Bad Request           — Validation failure, malformed JSON
    401 Unauthorized          — Missing or invalid authentication
    403 Forbidden             — Authenticated but not authorized
    404 Not Found             — Resource doesn't exist
    409 Conflict              — Duplicate entry, state conflict
    422 Unprocessable Entity  — Semantically invalid (valid JSON, bad data)
    429 Too Many Requests     — Rate limit exceeded
    
    # Server Errors
    500 Internal Server Error — Unexpected failure (never expose details)
    502 Bad Gateway           — Upstream service failed
    503 Service Unavailable   — Temporary overload, include Retry-After
    

    Common Mistakes

    # BAD: 200 for everything
    { "status": 200, "success": false, "error": "Not found" }
    
    # GOOD: Use HTTP status codes semantically
    HTTP/1.1 404 Not Found
    { "error": { "code": "not_found", "message": "User not found" } }
    
    # BAD: 500 for validation errors
    # GOOD: 400 or 422 with field-level details
    
    # BAD: 200 for created resources
    # GOOD: 201 with Location header
    HTTP/1.1 201 Created
    Location: /api/v1/users/abc-123
    

    Response Format

    Success Response

    {
      "data": {
        "id": "abc-123",
        "email": "alice@example.com",
        "name": "Alice",
        "created_at": "2025-01-15T10:30:00Z"
      }
    }
    

    Collection Response (with Pagination)

    {
      "data": [
        { "id": "abc-123", "name": "Alice" },
        { "id": "def-456", "name": "Bob" }
      ],
      "meta": {
        "total": 142,
        "page": 1,
        "per_page": 20,
        "total_pages": 8
      },
      "links": {
        "self": "/api/v1/users?page=1&per_page=20",
        "next": "/api/v1/users?page=2&per_page=20",
        "last": "/api/v1/users?page=8&per_page=20"
      }
    }
    

    Error Response

    {
      "error": {
        "code": "validation_error",
        "message": "Request validation failed",
        "details": [
          {
            "field": "email",
            "message": "Must be a valid email address",
            "code": "invalid_format"
          },
          {
            "field": "age",
            "message": "Must be between 0 and 150",
            "code": "out_of_range"
          }
        ]
      }
    }
    

    Response Envelope Variants

    // Option A: Envelope with data wrapper (recommended for public APIs)
    interface ApiResponse<T> {
      data: T;
      meta?: PaginationMeta;
      links?: PaginationLinks;
    }
    
    interface ApiError {
      error: {
        code: string;
        message: string;
        details?: FieldError[];
      };
    }
    
    // Option B: Flat response (simpler, common for internal APIs)
    // Success: just return the resource directly
    // Error: return error object
    // Distinguish by HTTP status code
    

    Pagination

    Offset-Based (Simple)

    GET /api/v1/users?page=2&per_page=20
    
    # Implementation
    SELECT * FROM users
    ORDER BY created_at DESC
    LIMIT 20 OFFSET 20;
    

    Pros: Easy to implement, supports "jump to page N" Cons: Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts

    Cursor-Based (Scalable)

    GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
    
    # Implementation
    SELECT * FROM users
    WHERE id > :cursor_id
    ORDER BY id ASC
    LIMIT 21;  -- fetch one extra to determine has_next
    
    {
      "data": [...],
      "meta": {
        "has_next": true,
        "next_cursor": "eyJpZCI6MTQzfQ"
      }
    }
    

    Pros: Consistent performance regardless of position, stable with concurrent inserts Cons: Cannot jump to arbitrary page, cursor is opaque

    When to Use Which

    Use CasePagination Type
    Admin dashboards, small datasets (<10K)Offset
    Infinite scroll, feeds, large datasetsCursor
    Public APIsCursor (default) with offset (optional)
    Search resultsOffset (users expect page numbers)

    Filtering, Sorting, and Search

    Filtering

    # Simple equality
    GET /api/v1/orders?status=active&customer_id=abc-123
    
    # Comparison operators (use bracket notation)
    GET /api/v1/products?price[gte]=10&price[lte]=100
    GET /api/v1/orders?created_at[after]=2025-01-01
    
    # Multiple values (comma-separated)
    GET /api/v1/products?category=electronics,clothing
    
    # Nested fields (dot notation)
    GET /api/v1/orders?customer.country=US
    

    Sorting

    # Single field (prefix - for descending)
    GET /api/v1/products?sort=-created_at
    
    # Multiple fields (comma-separated)
    GET /api/v1/products?sort=-featured,price,-created_at
    

    Full-Text Search

    # Search query parameter
    GET /api/v1/products?q=wireless+headphones
    
    # Field-specific search
    GET /api/v1/users?email=alice
    

    Sparse Fieldsets

    # Return only specified fields (reduces payload)
    GET /api/v1/users?fields=id,name,email
    GET /api/v1/orders?fields=id,total,status&include=customer.name
    

    Authentication and Authorization

    Token-Based Auth

    # Bearer token in Authorization header
    GET /api/v1/users
    Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
    
    # API key (for server-to-server)
    GET /api/v1/data
    X-API-Key: sk_live_abc123
    

    Authorization Patterns

    // Resource-level: check ownership
    app.get("/api/v1/orders/:id", async (req, res) => {
      const order = await Order.findById(req.params.id);
      if (!order) return res.status(404).json({ error: { code: "not_found" } });
      if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
      return res.json({ data: order });
    });
    
    // Role-based: check permissions
    app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
      await User.delete(req.params.id);
      return res.status(204).send();
    });
    

    Rate Limiting

    Headers

    HTTP/1.1 200 OK
    X-RateLimit-Limit: 100
    X-RateLimit-Remaining: 95
    X-RateLimit-Reset: 1640000000
    
    # When exceeded
    HTTP/1.1 429 Too Many Requests
    Retry-After: 60
    {
      "error": {
        "code": "rate_limit_exceeded",
        "message": "Rate limit exceeded. Try again in 60 seconds."
      }
    }
    

    Rate Limit Tiers

    TierLimitWindowUse Case
    Anonymous30/minPer IPPublic endpoints
    Authenticated100/minPer userStandard API access
    Premium1000/minPer API keyPaid API plans
    Internal10000/minPer serviceService-to-service

    Versioning

    URL Path Versioning (Recommended)

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

    Pros: Explicit, easy to route, cacheable Cons: URL changes between versions

    Header Versioning

    GET /api/users
    Accept: application/vnd.myapp.v2+json
    

    Pros: Clean URLs Cons: Harder to test, easy to forget

    Versioning Strategy

    1. Start with /api/v1/ — don't version until you need to
    2. Maintain at most 2 active versions (current + previous)
    3. Deprecation timeline:
       - Announce deprecation (6 months notice for public APIs)
       - Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
       - Return 410 Gone after sunset date
    4. Non-breaking changes don't need a new version:
       - Adding new fields to responses
       - Adding new optional query parameters
       - Adding new endpoints
    5. Breaking changes require a new version:
       - Removing or renaming fields
       - Changing field types
       - Changing URL structure
       - Changing authentication method
    

    Implementation Patterns

    TypeScript (Next.js API Route)

    import { z } from "zod";
    import { NextRequest, NextResponse } from "next/server";
    
    const createUserSchema = z.object({
      email: z.string().email(),
      name: z.string().min(1).max(100),
    });
    
    export async function POST(req: NextRequest) {
      const body = await req.json();
      const parsed = createUserSchema.safeParse(body);
    
      if (!parsed.success) {
        return NextResponse.json({
          error: {
            code: "validation_error",
            message: "Request validation failed",
            details: parsed.error.issues.map(i => ({
              field: i.path.join("."),
              message: i.message,
              code: i.code,
            })),
          },
        }, { status: 422 });
      }
    
      const user = await createUser(parsed.data);
    
      return NextResponse.json(
        { data: user },
        {
          status: 201,
          headers: { Location: `/api/v1/users/${user.id}` },
        },
      );
    }
    

    Python (Django REST Framework)

    from rest_framework import serializers, viewsets, status
    from rest_framework.response import Response
    
    class CreateUserSerializer(serializers.Serializer):
        email = serializers.EmailField()
        name = serializers.CharField(max_length=100)
    
    class UserSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            fields = ["id", "email", "name", "created_at"]
    
    class UserViewSet(viewsets.ModelViewSet):
        serializer_class = UserSerializer
        permission_classes = [IsAuthenticated]
    
        def get_serializer_class(self):
            if self.action == "create":
                return CreateUserSerializer
            return UserSerializer
    
        def create(self, request):
            serializer = CreateUserSerializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            user = UserService.create(**serializer.validated_data)
            return Response(
                {"data": UserSerializer(user).data},
                status=status.HTTP_201_CREATED,
                headers={"Location": f"/api/v1/users/{user.id}"},
            )
    

    Go (net/http)

    func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
        var req CreateUserRequest
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
            writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
            return
        }
    
        if err := req.Validate(); err != nil {
            writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
            return
        }
    
        user, err := h.service.Create(r.Context(), req)
        if err != nil {
            switch {
            case errors.Is(err, domain.ErrEmailTaken):
                writeError(w, http.StatusConflict, "email_taken", "Email already registered")
            default:
                writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
            }
            return
        }
    
        w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
        writeJSON(w, http.StatusCreated, map[string]any{"data": user})
    }
    

    API Design Checklist

    Before shipping a new endpoint:

    • Resource URL follows naming conventions (plural, kebab-case, no verbs)
    • Correct HTTP method used (GET for reads, POST for creates, etc.)
    • Appropriate status codes returned (not 200 for everything)
    • Input validated with schema (Zod, Pydantic, Bean Validation)
    • Error responses follow standard format with codes and messages
    • Pagination implemented for list endpoints (cursor or offset)
    • Authentication required (or explicitly marked as public)
    • Authorization checked (user can only access their own resources)
    • Rate limiting configured
    • Response does not leak internal details (stack traces, SQL errors)
    • Consistent naming with existing endpoints (camelCase vs snake_case)
    • Documented (OpenAPI/Swagger spec updated)

    Alternatives

    Compare before choosing