agentskills.codes
AP

API Design Skill

Design APIs that developers love to use.

Install

mkdir -p .claude/skills/api-design-skill && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14995" && unzip -o skill.zip -d .claude/skills/api-design-skill && rm skill.zip

Installs to .claude/skills/api-design-skill

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Design APIs that developers love to use.
40 charsno explicit “when” trigger

About this skill

API Design Skill

Design APIs that developers love to use.

Core Principle

A good API is intuitive, consistent, and hard to misuse. Design for the consumer, not the implementation.

REST Fundamentals

Resource Naming

GoodBadWhy
/users/getUsersNouns, not verbs
/users/123/user?id=123Path params for identity
/users/123/orders/getUserOrdersHierarchical resources
/search?q=term/search/termQuery params for filters

HTTP Methods

MethodPurposeIdempotentSafe
GETRead resource
POSTCreate resource
PUTReplace resource
PATCHPartial update❌*
DELETERemove resource

*PATCH can be idempotent if designed carefully

Status Codes

CodeMeaningWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestValidation error, malformed request
401UnauthorizedMissing/invalid authentication
403ForbiddenAuthenticated but not allowed
404Not FoundResource doesn't exist
409ConflictState conflict (duplicate, version)
422UnprocessableValid syntax, invalid semantics
429Too Many RequestsRate limited
500Internal ErrorServer bug (never expose details)

Contract-First Design

The Process

  1. Define the contract (OpenAPI/Swagger)
  2. Review with consumers before coding
  3. Generate server stubs from contract
  4. Implement business logic
  5. Validate responses against contract

OpenAPI Skeleton

openapi: 3.0.3
info:
  title: My API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserList'
components:
  schemas:
    User:
      type: object
      required: [id, email]
      properties:
        id:
          type: string
        email:
          type: string
          format: email

Versioning Strategies

StrategyExampleProsCons
URL Path/v1/usersExplicit, easy routingURL pollution
HeaderAccept: application/vnd.api.v1+jsonClean URLsHidden, harder to test
Query Param/users?version=1Explicit, flexibleLooks like filter

Recommendation: URL path for major versions. It's explicit and easy.

Version Lifecycle

  1. Active: Current version, fully supported
  2. Deprecated: Works but sunset announced
  3. Sunset: Removed, returns 410 Gone

Pagination Patterns

Offset-Based

GET /users?offset=40&limit=20

{
  "data": [...],
  "pagination": {
    "offset": 40,
    "limit": 20,
    "total": 150
  }
}
  • ✅ Simple, familiar
  • ❌ Inconsistent on fast-changing data

Cursor-Based

GET /users?cursor=abc123&limit=20

{
  "data": [...],
  "pagination": {
    "next_cursor": "def456",
    "has_more": true
  }
}
  • ✅ Consistent, performant
  • ❌ No random access

Error Response Design

Consistent Structure

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request parameters",
    "details": [
      {
        "field": "email",
        "message": "Must be valid email format"
      }
    ],
    "request_id": "req_abc123"
  }
}

Error Principles

  • Machine-readable code for programmatic handling
  • Human-readable message for debugging
  • Request ID for support correlation
  • Never expose stack traces or internal details

Request/Response Design

Consistency Rules

  • Use camelCase or snake_case—pick one, stick to it
  • Timestamps in ISO 8601: 2026-02-01T14:30:00Z
  • IDs as strings (future-proof for UUIDs)
  • Envelope responses: { "data": ..., "meta": ... }

Partial Responses

GET /users/123?fields=id,name,email

Reduces payload, improves performance.

Bulk Operations

POST /users/bulk
{
  "operations": [
    { "method": "create", "data": {...} },
    { "method": "update", "id": "123", "data": {...} }
  ]
}

Caching Strategies

HTTP Cache Headers

HeaderPurposeExample
Cache-ControlCaching directivesmax-age=3600, private
ETagContent fingerprint"abc123"
Last-ModifiedTimestamp of changeSat, 01 Feb 2026 12:00:00 GMT
VaryCache key factorsAccept, Authorization

Cache-Control Directives

# Public, cacheable for 1 hour
Cache-Control: public, max-age=3600

# Private (user-specific), cacheable for 5 minutes
Cache-Control: private, max-age=300

# No caching at all
Cache-Control: no-store

# Cache but revalidate every time
Cache-Control: no-cache

# Stale content OK while revalidating
Cache-Control: max-age=60, stale-while-revalidate=30

ETag Validation Flow

# First request
GET /users/123
→ 200 OK
   ETag: "v1-abc123"
   Cache-Control: private, max-age=60

# Subsequent request (after cache expires)
GET /users/123
If-None-Match: "v1-abc123"
→ 304 Not Modified (cache still valid)
   OR
→ 200 OK with new ETag (content changed)

Caching Decision Matrix

Resource TypeCache StrategyTTL
Static configpublic, max-ageHours/days
User profileprivate, max-ageMinutes
Real-time datano-storeNone
Search resultsprivate, max-ageSeconds
Public listingspublic, s-maxageMinutes

Application-Level Caching

// Cache key design
const cacheKey = `${resource}:${id}:${version}`;

// Cache-aside pattern
async function getUser(id: string) {
  const cached = await cache.get(`user:${id}`);
  if (cached) return cached;

  const user = await db.users.findById(id);
  await cache.set(`user:${id}`, user, { ttl: 300 });
  return user;
}

// Cache invalidation on write
async function updateUser(id: string, data: UserUpdate) {
  const user = await db.users.update(id, data);
  await cache.delete(`user:${id}`);
  await cache.delete(`users:list:*`); // Invalidate list caches
  return user;
}

Cache Invalidation Strategies

StrategyWhen to UseComplexity
TTL expiryLow-stakes dataLow
Event-drivenCritical consistencyMedium
Version tagsImmutable resourcesLow
Cache bustingStatic assetsLow
Write-throughAlways currentHigh

Rate Limiting

Why Rate Limit?

  • Protect resources: Prevent server overload
  • Ensure fairness: No single client monopolizes
  • Cost control: Limit expensive operations
  • Security: Mitigate DoS and brute force

Common Algorithms

AlgorithmDescriptionBest For
Fixed WindowX requests per minuteSimple APIs
Sliding WindowRolling time windowSmoother limits
Token BucketBurst-friendly with refillFlexible patterns
Leaky BucketSmooth output rateConsistent throughput

Token Bucket Example

class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private capacity: number,      // Max burst size
    private refillRate: number,    // Tokens per second
  ) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }

  consume(tokens: number = 1): boolean {
    this.refill();
    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    return false;
  }

  private refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(
      this.capacity,
      this.tokens + elapsed * this.refillRate
    );
    this.lastRefill = now;
  }
}

Rate Limit Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706792400

# When exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706792400

Rate Limit Tiers

TierLimitUse Case
Anonymous60/hourPublic exploration
Authenticated1000/hourNormal usage
Premium10000/hourPower users
InternalUnlimitedService-to-service

Rate Limit Scopes

// Per-user limiting
const userKey = `ratelimit:user:${userId}`;

// Per-IP limiting (for anonymous)
const ipKey = `ratelimit:ip:${clientIp}`;

// Per-endpoint limiting
const endpointKey = `ratelimit:${method}:${path}`;

// Combined (most flexible)
const combinedKey = `ratelimit:${userId}:${method}:${path}`;

Client-Side Rate Limit Handling

async function apiCallWithRetry(request: Request): Promise<Response> {
  const response = await fetch(request);

  if (response.status === 429) {
    const retryAfter = parseInt(
      response.headers.get('Retry-After') || '60'
    );

    console.log(`Rate limited. Retrying in ${retryAfter}s`);
    await sleep(retryAfter * 1000);
    return apiCallWithRetry(request);
  }

  return response;
}

// Proactive rate limiting
class RateLimitedClient {
  private remaining: number = Infinity;
  private resetTime: number = 0;

  async request(url: string): Promise<Response> {
    // Wait if we know we're out of quota
    if (this.remaining <= 0 && Date.now() < this.resetTime) {
      await sleep(this.resetTime - Date.now());
    }

    const response = await fetch(url);

    // Update tracking from headers
    this.remaining = parseInt(
      response.headers.get('X-RateLimit-Remaining') || '100'
    );
    this.resetTime = parseInt(
      response.headers.

---

*Content truncated.*

Search skills

Search the agent skills registry