{
  "schemaVersion": "1.0",
  "item": {
    "slug": "api-design",
    "name": "Api Design",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/shateily/api-design",
    "canonicalUrl": "https://clawhub.ai/shateily/api-design",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/api-design",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=api-design",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "slug": "api-design",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T00:27:15.822Z",
      "expiresAt": "2026-05-07T00:27:15.822Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=api-design",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=api-design",
        "contentDisposition": "attachment; filename=\"api-design-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null,
        "slug": "api-design"
      },
      "scope": "item",
      "summary": "Item download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this item.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/api-design"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/api-design",
    "agentPageUrl": "https://openagent3.xyz/skills/api-design/agent",
    "manifestUrl": "https://openagent3.xyz/skills/api-design/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/api-design/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "API Design Patterns",
        "body": "Conventions and best practices for designing consistent, developer-friendly REST APIs."
      },
      {
        "title": "When to Activate",
        "body": "Designing new API endpoints\nReviewing existing API contracts\nAdding pagination, filtering, or sorting\nImplementing error handling for APIs\nPlanning API versioning strategy\nBuilding public or partner-facing APIs"
      },
      {
        "title": "URL Structure",
        "body": "# Resources are nouns, plural, lowercase, kebab-case\nGET    /api/v1/users\nGET    /api/v1/users/:id\nPOST   /api/v1/users\nPUT    /api/v1/users/:id\nPATCH  /api/v1/users/:id\nDELETE /api/v1/users/:id\n\n# Sub-resources for relationships\nGET    /api/v1/users/:id/orders\nPOST   /api/v1/users/:id/orders\n\n# Actions that don't map to CRUD (use verbs sparingly)\nPOST   /api/v1/orders/:id/cancel\nPOST   /api/v1/auth/login\nPOST   /api/v1/auth/refresh"
      },
      {
        "title": "Naming Rules",
        "body": "# GOOD\n/api/v1/team-members          # kebab-case for multi-word resources\n/api/v1/orders?status=active  # query params for filtering\n/api/v1/users/123/orders      # nested resources for ownership\n\n# BAD\n/api/v1/getUsers              # verb in URL\n/api/v1/user                  # singular (use plural)\n/api/v1/team_members          # snake_case in URLs\n/api/v1/users/123/getOrders   # verb in nested resource"
      },
      {
        "title": "Method Semantics",
        "body": "MethodIdempotentSafeUse ForGETYesYesRetrieve resourcesPOSTNoNoCreate resources, trigger actionsPUTYesNoFull replacement of a resourcePATCHNo*NoPartial update of a resourceDELETEYesNoRemove a resource\n\n*PATCH can be made idempotent with proper implementation"
      },
      {
        "title": "Status Code Reference",
        "body": "# Success\n200 OK                    — GET, PUT, PATCH (with response body)\n201 Created               — POST (include Location header)\n204 No Content            — DELETE, PUT (no response body)\n\n# Client Errors\n400 Bad Request           — Validation failure, malformed JSON\n401 Unauthorized          — Missing or invalid authentication\n403 Forbidden             — Authenticated but not authorized\n404 Not Found             — Resource doesn't exist\n409 Conflict              — Duplicate entry, state conflict\n422 Unprocessable Entity  — Semantically invalid (valid JSON, bad data)\n429 Too Many Requests     — Rate limit exceeded\n\n# Server Errors\n500 Internal Server Error — Unexpected failure (never expose details)\n502 Bad Gateway           — Upstream service failed\n503 Service Unavailable   — Temporary overload, include Retry-After"
      },
      {
        "title": "Common Mistakes",
        "body": "# BAD: 200 for everything\n{ \"status\": 200, \"success\": false, \"error\": \"Not found\" }\n\n# GOOD: Use HTTP status codes semantically\nHTTP/1.1 404 Not Found\n{ \"error\": { \"code\": \"not_found\", \"message\": \"User not found\" } }\n\n# BAD: 500 for validation errors\n# GOOD: 400 or 422 with field-level details\n\n# BAD: 200 for created resources\n# GOOD: 201 with Location header\nHTTP/1.1 201 Created\nLocation: /api/v1/users/abc-123"
      },
      {
        "title": "Success Response",
        "body": "{\n  \"data\": {\n    \"id\": \"abc-123\",\n    \"email\": \"alice@example.com\",\n    \"name\": \"Alice\",\n    \"created_at\": \"2025-01-15T10:30:00Z\"\n  }\n}"
      },
      {
        "title": "Collection Response (with Pagination)",
        "body": "{\n  \"data\": [\n    { \"id\": \"abc-123\", \"name\": \"Alice\" },\n    { \"id\": \"def-456\", \"name\": \"Bob\" }\n  ],\n  \"meta\": {\n    \"total\": 142,\n    \"page\": 1,\n    \"per_page\": 20,\n    \"total_pages\": 8\n  },\n  \"links\": {\n    \"self\": \"/api/v1/users?page=1&per_page=20\",\n    \"next\": \"/api/v1/users?page=2&per_page=20\",\n    \"last\": \"/api/v1/users?page=8&per_page=20\"\n  }\n}"
      },
      {
        "title": "Error Response",
        "body": "{\n  \"error\": {\n    \"code\": \"validation_error\",\n    \"message\": \"Request validation failed\",\n    \"details\": [\n      {\n        \"field\": \"email\",\n        \"message\": \"Must be a valid email address\",\n        \"code\": \"invalid_format\"\n      },\n      {\n        \"field\": \"age\",\n        \"message\": \"Must be between 0 and 150\",\n        \"code\": \"out_of_range\"\n      }\n    ]\n  }\n}"
      },
      {
        "title": "Response Envelope Variants",
        "body": "// Option A: Envelope with data wrapper (recommended for public APIs)\ninterface ApiResponse<T> {\n  data: T;\n  meta?: PaginationMeta;\n  links?: PaginationLinks;\n}\n\ninterface ApiError {\n  error: {\n    code: string;\n    message: string;\n    details?: FieldError[];\n  };\n}\n\n// Option B: Flat response (simpler, common for internal APIs)\n// Success: just return the resource directly\n// Error: return error object\n// Distinguish by HTTP status code"
      },
      {
        "title": "Offset-Based (Simple)",
        "body": "GET /api/v1/users?page=2&per_page=20\n\n# Implementation\nSELECT * FROM users\nORDER BY created_at DESC\nLIMIT 20 OFFSET 20;\n\nPros: Easy to implement, supports \"jump to page N\"\nCons: Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts"
      },
      {
        "title": "Cursor-Based (Scalable)",
        "body": "GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20\n\n# Implementation\nSELECT * FROM users\nWHERE id > :cursor_id\nORDER BY id ASC\nLIMIT 21;  -- fetch one extra to determine has_next\n\n{\n  \"data\": [...],\n  \"meta\": {\n    \"has_next\": true,\n    \"next_cursor\": \"eyJpZCI6MTQzfQ\"\n  }\n}\n\nPros: Consistent performance regardless of position, stable with concurrent inserts\nCons: Cannot jump to arbitrary page, cursor is opaque"
      },
      {
        "title": "When to Use Which",
        "body": "Use CasePagination TypeAdmin dashboards, small datasets (<10K)OffsetInfinite scroll, feeds, large datasetsCursorPublic APIsCursor (default) with offset (optional)Search resultsOffset (users expect page numbers)"
      },
      {
        "title": "Filtering",
        "body": "# Simple equality\nGET /api/v1/orders?status=active&customer_id=abc-123\n\n# Comparison operators (use bracket notation)\nGET /api/v1/products?price[gte]=10&price[lte]=100\nGET /api/v1/orders?created_at[after]=2025-01-01\n\n# Multiple values (comma-separated)\nGET /api/v1/products?category=electronics,clothing\n\n# Nested fields (dot notation)\nGET /api/v1/orders?customer.country=US"
      },
      {
        "title": "Sorting",
        "body": "# Single field (prefix - for descending)\nGET /api/v1/products?sort=-created_at\n\n# Multiple fields (comma-separated)\nGET /api/v1/products?sort=-featured,price,-created_at"
      },
      {
        "title": "Full-Text Search",
        "body": "# Search query parameter\nGET /api/v1/products?q=wireless+headphones\n\n# Field-specific search\nGET /api/v1/users?email=alice"
      },
      {
        "title": "Sparse Fieldsets",
        "body": "# Return only specified fields (reduces payload)\nGET /api/v1/users?fields=id,name,email\nGET /api/v1/orders?fields=id,total,status&include=customer.name"
      },
      {
        "title": "Token-Based Auth",
        "body": "# Bearer token in Authorization header\nGET /api/v1/users\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIs...\n\n# API key (for server-to-server)\nGET /api/v1/data\nX-API-Key: sk_live_abc123"
      },
      {
        "title": "Authorization Patterns",
        "body": "// Resource-level: check ownership\napp.get(\"/api/v1/orders/:id\", async (req, res) => {\n  const order = await Order.findById(req.params.id);\n  if (!order) return res.status(404).json({ error: { code: \"not_found\" } });\n  if (order.userId !== req.user.id) return res.status(403).json({ error: { code: \"forbidden\" } });\n  return res.json({ data: order });\n});\n\n// Role-based: check permissions\napp.delete(\"/api/v1/users/:id\", requireRole(\"admin\"), async (req, res) => {\n  await User.delete(req.params.id);\n  return res.status(204).send();\n});"
      },
      {
        "title": "Headers",
        "body": "HTTP/1.1 200 OK\nX-RateLimit-Limit: 100\nX-RateLimit-Remaining: 95\nX-RateLimit-Reset: 1640000000\n\n# When exceeded\nHTTP/1.1 429 Too Many Requests\nRetry-After: 60\n{\n  \"error\": {\n    \"code\": \"rate_limit_exceeded\",\n    \"message\": \"Rate limit exceeded. Try again in 60 seconds.\"\n  }\n}"
      },
      {
        "title": "Rate Limit Tiers",
        "body": "TierLimitWindowUse CaseAnonymous30/minPer IPPublic endpointsAuthenticated100/minPer userStandard API accessPremium1000/minPer API keyPaid API plansInternal10000/minPer serviceService-to-service"
      },
      {
        "title": "URL Path Versioning (Recommended)",
        "body": "/api/v1/users\n/api/v2/users\n\nPros: Explicit, easy to route, cacheable\nCons: URL changes between versions"
      },
      {
        "title": "Header Versioning",
        "body": "GET /api/users\nAccept: application/vnd.myapp.v2+json\n\nPros: Clean URLs\nCons: Harder to test, easy to forget"
      },
      {
        "title": "Versioning Strategy",
        "body": "1. Start with /api/v1/ — don't version until you need to\n2. Maintain at most 2 active versions (current + previous)\n3. Deprecation timeline:\n   - Announce deprecation (6 months notice for public APIs)\n   - Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT\n   - Return 410 Gone after sunset date\n4. Non-breaking changes don't need a new version:\n   - Adding new fields to responses\n   - Adding new optional query parameters\n   - Adding new endpoints\n5. Breaking changes require a new version:\n   - Removing or renaming fields\n   - Changing field types\n   - Changing URL structure\n   - Changing authentication method"
      },
      {
        "title": "TypeScript (Next.js API Route)",
        "body": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nconst createUserSchema = z.object({\n  email: z.string().email(),\n  name: z.string().min(1).max(100),\n});\n\nexport async function POST(req: NextRequest) {\n  const body = await req.json();\n  const parsed = createUserSchema.safeParse(body);\n\n  if (!parsed.success) {\n    return NextResponse.json({\n      error: {\n        code: \"validation_error\",\n        message: \"Request validation failed\",\n        details: parsed.error.issues.map(i => ({\n          field: i.path.join(\".\"),\n          message: i.message,\n          code: i.code,\n        })),\n      },\n    }, { status: 422 });\n  }\n\n  const user = await createUser(parsed.data);\n\n  return NextResponse.json(\n    { data: user },\n    {\n      status: 201,\n      headers: { Location: `/api/v1/users/${user.id}` },\n    },\n  );\n}"
      },
      {
        "title": "Python (Django REST Framework)",
        "body": "from rest_framework import serializers, viewsets, status\nfrom rest_framework.response import Response\n\nclass CreateUserSerializer(serializers.Serializer):\n    email = serializers.EmailField()\n    name = serializers.CharField(max_length=100)\n\nclass UserSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = User\n        fields = [\"id\", \"email\", \"name\", \"created_at\"]\n\nclass UserViewSet(viewsets.ModelViewSet):\n    serializer_class = UserSerializer\n    permission_classes = [IsAuthenticated]\n\n    def get_serializer_class(self):\n        if self.action == \"create\":\n            return CreateUserSerializer\n        return UserSerializer\n\n    def create(self, request):\n        serializer = CreateUserSerializer(data=request.data)\n        serializer.is_valid(raise_exception=True)\n        user = UserService.create(**serializer.validated_data)\n        return Response(\n            {\"data\": UserSerializer(user).data},\n            status=status.HTTP_201_CREATED,\n            headers={\"Location\": f\"/api/v1/users/{user.id}\"},\n        )"
      },
      {
        "title": "Go (net/http)",
        "body": "func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {\n    var req CreateUserRequest\n    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n        writeError(w, http.StatusBadRequest, \"invalid_json\", \"Invalid request body\")\n        return\n    }\n\n    if err := req.Validate(); err != nil {\n        writeError(w, http.StatusUnprocessableEntity, \"validation_error\", err.Error())\n        return\n    }\n\n    user, err := h.service.Create(r.Context(), req)\n    if err != nil {\n        switch {\n        case errors.Is(err, domain.ErrEmailTaken):\n            writeError(w, http.StatusConflict, \"email_taken\", \"Email already registered\")\n        default:\n            writeError(w, http.StatusInternalServerError, \"internal_error\", \"Internal error\")\n        }\n        return\n    }\n\n    w.Header().Set(\"Location\", fmt.Sprintf(\"/api/v1/users/%s\", user.ID))\n    writeJSON(w, http.StatusCreated, map[string]any{\"data\": user})\n}"
      },
      {
        "title": "API Design Checklist",
        "body": "Before shipping a new endpoint:\n\nResource URL follows naming conventions (plural, kebab-case, no verbs)\n Correct HTTP method used (GET for reads, POST for creates, etc.)\n Appropriate status codes returned (not 200 for everything)\n Input validated with schema (Zod, Pydantic, Bean Validation)\n Error responses follow standard format with codes and messages\n Pagination implemented for list endpoints (cursor or offset)\n Authentication required (or explicitly marked as public)\n Authorization checked (user can only access their own resources)\n Rate limiting configured\n Response does not leak internal details (stack traces, SQL errors)\n Consistent naming with existing endpoints (camelCase vs snake_case)\n Documented (OpenAPI/Swagger spec updated)"
      }
    ],
    "body": "API Design Patterns\n\nConventions and best practices for designing consistent, developer-friendly REST APIs.\n\nWhen to Activate\nDesigning new API endpoints\nReviewing existing API contracts\nAdding pagination, filtering, or sorting\nImplementing error handling for APIs\nPlanning API versioning strategy\nBuilding public or partner-facing APIs\nResource Design\nURL Structure\n# Resources are nouns, plural, lowercase, kebab-case\nGET    /api/v1/users\nGET    /api/v1/users/:id\nPOST   /api/v1/users\nPUT    /api/v1/users/:id\nPATCH  /api/v1/users/:id\nDELETE /api/v1/users/:id\n\n# Sub-resources for relationships\nGET    /api/v1/users/:id/orders\nPOST   /api/v1/users/:id/orders\n\n# Actions that don't map to CRUD (use verbs sparingly)\nPOST   /api/v1/orders/:id/cancel\nPOST   /api/v1/auth/login\nPOST   /api/v1/auth/refresh\n\nNaming Rules\n# GOOD\n/api/v1/team-members          # kebab-case for multi-word resources\n/api/v1/orders?status=active  # query params for filtering\n/api/v1/users/123/orders      # nested resources for ownership\n\n# BAD\n/api/v1/getUsers              # verb in URL\n/api/v1/user                  # singular (use plural)\n/api/v1/team_members          # snake_case in URLs\n/api/v1/users/123/getOrders   # verb in nested resource\n\nHTTP Methods and Status Codes\nMethod Semantics\nMethod\tIdempotent\tSafe\tUse For\nGET\tYes\tYes\tRetrieve resources\nPOST\tNo\tNo\tCreate resources, trigger actions\nPUT\tYes\tNo\tFull replacement of a resource\nPATCH\tNo*\tNo\tPartial update of a resource\nDELETE\tYes\tNo\tRemove a resource\n\n*PATCH can be made idempotent with proper implementation\n\nStatus Code Reference\n# Success\n200 OK                    — GET, PUT, PATCH (with response body)\n201 Created               — POST (include Location header)\n204 No Content            — DELETE, PUT (no response body)\n\n# Client Errors\n400 Bad Request           — Validation failure, malformed JSON\n401 Unauthorized          — Missing or invalid authentication\n403 Forbidden             — Authenticated but not authorized\n404 Not Found             — Resource doesn't exist\n409 Conflict              — Duplicate entry, state conflict\n422 Unprocessable Entity  — Semantically invalid (valid JSON, bad data)\n429 Too Many Requests     — Rate limit exceeded\n\n# Server Errors\n500 Internal Server Error — Unexpected failure (never expose details)\n502 Bad Gateway           — Upstream service failed\n503 Service Unavailable   — Temporary overload, include Retry-After\n\nCommon Mistakes\n# BAD: 200 for everything\n{ \"status\": 200, \"success\": false, \"error\": \"Not found\" }\n\n# GOOD: Use HTTP status codes semantically\nHTTP/1.1 404 Not Found\n{ \"error\": { \"code\": \"not_found\", \"message\": \"User not found\" } }\n\n# BAD: 500 for validation errors\n# GOOD: 400 or 422 with field-level details\n\n# BAD: 200 for created resources\n# GOOD: 201 with Location header\nHTTP/1.1 201 Created\nLocation: /api/v1/users/abc-123\n\nResponse Format\nSuccess Response\n{\n  \"data\": {\n    \"id\": \"abc-123\",\n    \"email\": \"alice@example.com\",\n    \"name\": \"Alice\",\n    \"created_at\": \"2025-01-15T10:30:00Z\"\n  }\n}\n\nCollection Response (with Pagination)\n{\n  \"data\": [\n    { \"id\": \"abc-123\", \"name\": \"Alice\" },\n    { \"id\": \"def-456\", \"name\": \"Bob\" }\n  ],\n  \"meta\": {\n    \"total\": 142,\n    \"page\": 1,\n    \"per_page\": 20,\n    \"total_pages\": 8\n  },\n  \"links\": {\n    \"self\": \"/api/v1/users?page=1&per_page=20\",\n    \"next\": \"/api/v1/users?page=2&per_page=20\",\n    \"last\": \"/api/v1/users?page=8&per_page=20\"\n  }\n}\n\nError Response\n{\n  \"error\": {\n    \"code\": \"validation_error\",\n    \"message\": \"Request validation failed\",\n    \"details\": [\n      {\n        \"field\": \"email\",\n        \"message\": \"Must be a valid email address\",\n        \"code\": \"invalid_format\"\n      },\n      {\n        \"field\": \"age\",\n        \"message\": \"Must be between 0 and 150\",\n        \"code\": \"out_of_range\"\n      }\n    ]\n  }\n}\n\nResponse Envelope Variants\n// Option A: Envelope with data wrapper (recommended for public APIs)\ninterface ApiResponse<T> {\n  data: T;\n  meta?: PaginationMeta;\n  links?: PaginationLinks;\n}\n\ninterface ApiError {\n  error: {\n    code: string;\n    message: string;\n    details?: FieldError[];\n  };\n}\n\n// Option B: Flat response (simpler, common for internal APIs)\n// Success: just return the resource directly\n// Error: return error object\n// Distinguish by HTTP status code\n\nPagination\nOffset-Based (Simple)\nGET /api/v1/users?page=2&per_page=20\n\n# Implementation\nSELECT * FROM users\nORDER BY created_at DESC\nLIMIT 20 OFFSET 20;\n\n\nPros: Easy to implement, supports \"jump to page N\" Cons: Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts\n\nCursor-Based (Scalable)\nGET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20\n\n# Implementation\nSELECT * FROM users\nWHERE id > :cursor_id\nORDER BY id ASC\nLIMIT 21;  -- fetch one extra to determine has_next\n\n{\n  \"data\": [...],\n  \"meta\": {\n    \"has_next\": true,\n    \"next_cursor\": \"eyJpZCI6MTQzfQ\"\n  }\n}\n\n\nPros: Consistent performance regardless of position, stable with concurrent inserts Cons: Cannot jump to arbitrary page, cursor is opaque\n\nWhen to Use Which\nUse Case\tPagination Type\nAdmin dashboards, small datasets (<10K)\tOffset\nInfinite scroll, feeds, large datasets\tCursor\nPublic APIs\tCursor (default) with offset (optional)\nSearch results\tOffset (users expect page numbers)\nFiltering, Sorting, and Search\nFiltering\n# Simple equality\nGET /api/v1/orders?status=active&customer_id=abc-123\n\n# Comparison operators (use bracket notation)\nGET /api/v1/products?price[gte]=10&price[lte]=100\nGET /api/v1/orders?created_at[after]=2025-01-01\n\n# Multiple values (comma-separated)\nGET /api/v1/products?category=electronics,clothing\n\n# Nested fields (dot notation)\nGET /api/v1/orders?customer.country=US\n\nSorting\n# Single field (prefix - for descending)\nGET /api/v1/products?sort=-created_at\n\n# Multiple fields (comma-separated)\nGET /api/v1/products?sort=-featured,price,-created_at\n\nFull-Text Search\n# Search query parameter\nGET /api/v1/products?q=wireless+headphones\n\n# Field-specific search\nGET /api/v1/users?email=alice\n\nSparse Fieldsets\n# Return only specified fields (reduces payload)\nGET /api/v1/users?fields=id,name,email\nGET /api/v1/orders?fields=id,total,status&include=customer.name\n\nAuthentication and Authorization\nToken-Based Auth\n# Bearer token in Authorization header\nGET /api/v1/users\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIs...\n\n# API key (for server-to-server)\nGET /api/v1/data\nX-API-Key: sk_live_abc123\n\nAuthorization Patterns\n// Resource-level: check ownership\napp.get(\"/api/v1/orders/:id\", async (req, res) => {\n  const order = await Order.findById(req.params.id);\n  if (!order) return res.status(404).json({ error: { code: \"not_found\" } });\n  if (order.userId !== req.user.id) return res.status(403).json({ error: { code: \"forbidden\" } });\n  return res.json({ data: order });\n});\n\n// Role-based: check permissions\napp.delete(\"/api/v1/users/:id\", requireRole(\"admin\"), async (req, res) => {\n  await User.delete(req.params.id);\n  return res.status(204).send();\n});\n\nRate Limiting\nHeaders\nHTTP/1.1 200 OK\nX-RateLimit-Limit: 100\nX-RateLimit-Remaining: 95\nX-RateLimit-Reset: 1640000000\n\n# When exceeded\nHTTP/1.1 429 Too Many Requests\nRetry-After: 60\n{\n  \"error\": {\n    \"code\": \"rate_limit_exceeded\",\n    \"message\": \"Rate limit exceeded. Try again in 60 seconds.\"\n  }\n}\n\nRate Limit Tiers\nTier\tLimit\tWindow\tUse Case\nAnonymous\t30/min\tPer IP\tPublic endpoints\nAuthenticated\t100/min\tPer user\tStandard API access\nPremium\t1000/min\tPer API key\tPaid API plans\nInternal\t10000/min\tPer service\tService-to-service\nVersioning\nURL Path Versioning (Recommended)\n/api/v1/users\n/api/v2/users\n\n\nPros: Explicit, easy to route, cacheable Cons: URL changes between versions\n\nHeader Versioning\nGET /api/users\nAccept: application/vnd.myapp.v2+json\n\n\nPros: Clean URLs Cons: Harder to test, easy to forget\n\nVersioning Strategy\n1. Start with /api/v1/ — don't version until you need to\n2. Maintain at most 2 active versions (current + previous)\n3. Deprecation timeline:\n   - Announce deprecation (6 months notice for public APIs)\n   - Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT\n   - Return 410 Gone after sunset date\n4. Non-breaking changes don't need a new version:\n   - Adding new fields to responses\n   - Adding new optional query parameters\n   - Adding new endpoints\n5. Breaking changes require a new version:\n   - Removing or renaming fields\n   - Changing field types\n   - Changing URL structure\n   - Changing authentication method\n\nImplementation Patterns\nTypeScript (Next.js API Route)\nimport { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nconst createUserSchema = z.object({\n  email: z.string().email(),\n  name: z.string().min(1).max(100),\n});\n\nexport async function POST(req: NextRequest) {\n  const body = await req.json();\n  const parsed = createUserSchema.safeParse(body);\n\n  if (!parsed.success) {\n    return NextResponse.json({\n      error: {\n        code: \"validation_error\",\n        message: \"Request validation failed\",\n        details: parsed.error.issues.map(i => ({\n          field: i.path.join(\".\"),\n          message: i.message,\n          code: i.code,\n        })),\n      },\n    }, { status: 422 });\n  }\n\n  const user = await createUser(parsed.data);\n\n  return NextResponse.json(\n    { data: user },\n    {\n      status: 201,\n      headers: { Location: `/api/v1/users/${user.id}` },\n    },\n  );\n}\n\nPython (Django REST Framework)\nfrom rest_framework import serializers, viewsets, status\nfrom rest_framework.response import Response\n\nclass CreateUserSerializer(serializers.Serializer):\n    email = serializers.EmailField()\n    name = serializers.CharField(max_length=100)\n\nclass UserSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = User\n        fields = [\"id\", \"email\", \"name\", \"created_at\"]\n\nclass UserViewSet(viewsets.ModelViewSet):\n    serializer_class = UserSerializer\n    permission_classes = [IsAuthenticated]\n\n    def get_serializer_class(self):\n        if self.action == \"create\":\n            return CreateUserSerializer\n        return UserSerializer\n\n    def create(self, request):\n        serializer = CreateUserSerializer(data=request.data)\n        serializer.is_valid(raise_exception=True)\n        user = UserService.create(**serializer.validated_data)\n        return Response(\n            {\"data\": UserSerializer(user).data},\n            status=status.HTTP_201_CREATED,\n            headers={\"Location\": f\"/api/v1/users/{user.id}\"},\n        )\n\nGo (net/http)\nfunc (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {\n    var req CreateUserRequest\n    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n        writeError(w, http.StatusBadRequest, \"invalid_json\", \"Invalid request body\")\n        return\n    }\n\n    if err := req.Validate(); err != nil {\n        writeError(w, http.StatusUnprocessableEntity, \"validation_error\", err.Error())\n        return\n    }\n\n    user, err := h.service.Create(r.Context(), req)\n    if err != nil {\n        switch {\n        case errors.Is(err, domain.ErrEmailTaken):\n            writeError(w, http.StatusConflict, \"email_taken\", \"Email already registered\")\n        default:\n            writeError(w, http.StatusInternalServerError, \"internal_error\", \"Internal error\")\n        }\n        return\n    }\n\n    w.Header().Set(\"Location\", fmt.Sprintf(\"/api/v1/users/%s\", user.ID))\n    writeJSON(w, http.StatusCreated, map[string]any{\"data\": user})\n}\n\nAPI Design Checklist\n\nBefore shipping a new endpoint:\n\n Resource URL follows naming conventions (plural, kebab-case, no verbs)\n Correct HTTP method used (GET for reads, POST for creates, etc.)\n Appropriate status codes returned (not 200 for everything)\n Input validated with schema (Zod, Pydantic, Bean Validation)\n Error responses follow standard format with codes and messages\n Pagination implemented for list endpoints (cursor or offset)\n Authentication required (or explicitly marked as public)\n Authorization checked (user can only access their own resources)\n Rate limiting configured\n Response does not leak internal details (stack traces, SQL errors)\n Consistent naming with existing endpoints (camelCase vs snake_case)\n Documented (OpenAPI/Swagger spec updated)"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/shateily/api-design",
    "publisherUrl": "https://clawhub.ai/shateily/api-design",
    "owner": "shateily",
    "version": "1.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/api-design",
    "downloadUrl": "https://openagent3.xyz/downloads/api-design",
    "agentUrl": "https://openagent3.xyz/skills/api-design/agent",
    "manifestUrl": "https://openagent3.xyz/skills/api-design/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/api-design/agent.md"
  }
}