api-design

REST API design patterns covering resource naming, HTTP semantics, pagination, filtering, error handling, and versioning. Resource URLs use plural nouns in kebab-case with semantic HTTP methods; status codes (201, 204, 400, 404, 422, 429) convey intent rather than embedding status in response bodies Supports offset-based pagination for small datasets and cursor-based pagination for scalable, feed-like interfaces; includes filtering via query parameters, sorting, and sparse fieldsets Error responses include structured codes, messages, and field-level details; success responses wrap data in consistent envelopes with optional metadata and pagination links Covers token and API key authentication, resource-level and role-based authorization patterns, and rate limiting with tier-based quotas and Retry-After headers Recommends URL path versioning (/api/v1/) with deprecation timelines; distinguishes breaking changes (new version required) from non-breaking additions (no version bump needed)

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill api-design
Run in your project or agent environment. Adjust flags if your CLI version differs.

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

Method

Idempotent

Safe

Use For

GET

Yes

Yes

Retrieve resources

POST

No

No

Create resources, trigger actions

PUT

Yes

No

Full replacement of a resource

PATCH

No*

No

Partial update of a resource

DELETE

Yes

No

Remove 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&#x26;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&#x26;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 Case

Pagination Type

Admin dashboards, small datasets (<10K)

Offset

Infinite scroll, feeds, large datasets

Cursor

Public APIs

Cursor (default) with offset (optional)

Search results

Offset (users expect page numbers)

Filtering, Sorting, and Search

Filtering

# Simple equality

GET /api/v1/orders?status=active&#x26;customer_id=abc-123

# Comparison operators (use bracket notation)

GET /api/v1/products?price[gte]=10&#x26;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&#x26;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

Tier

Limit

Window

Use Case

Anonymous

30/min

Per IP

Public endpoints

Authenticated

100/min

Per user

Standard API access

Premium

1000/min

Per API key

Paid API plans

Internal

10000/min

Per service

Service-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(&#x26;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)
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card