agentleFS
agenticlukeagenticluke/rest-api-designer-plus/skill/SKILL.md

api-design

Skill0 starsChanged 2 months agoMITView on GitHub

Design clear REST APIs with good names, status codes, page rules, filters, errors, versions, access checks, and rate limits.

192 other projects use a version of this, 52 of them edited.

Adopted by

Most recently taken up by

Used with

Projects with it also use db-design, everything-claude-code and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • yuexueyuSkill

    db-design

    数据库工程师技能 - Schema设计、索引优化、并发安全、数据库审计。当你涉及数据库表/字段/索引/SQL/迁移/MCP数据库操作、建表改表、写查询时必须使用此技能。即使用户只是说"加个字段"或"查下数据",也应触发。

    Also in 4 projects that use this

  • InfopibeSkill

    everything-claude-code

    Also in 9 projects that use this

  • mryeastcodeSkill

    everything-claude-code-conventions

    Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

    Also in 8 projects that use this

  • tpavanipradeepSkill

    nextjs-turbopack

    Next.js 16+ and Turbopack — incremental bundling, FS caching, dev speed, and when to use Turbopack vs webpack.

    Also in 15 projects that use this

  • Indium-AI-LabsSkill

    data-pipeline-design

    Design batch or incremental ETL and ELT pipelines and warehouse models: grain, partitioning, idempotent loads, late-arriving data, data quality tests, PII controls, and cost monitoring, before building them. Use when adding or changing a dbt model, warehouse table, scheduled transformation, or ingestion job. Not for transactional schema design (use the database-design skill) or event-sourced stores (use the event-sourcing-architecture skill).

    Also in 3 projects that use this

  • thejordanleopoldSkill

    third-party-integration

    Use when integrating with external APIs, payment processors (Stripe), webhooks, or third-party services. Use when implementing API clients with authentication and rate limiting, handling webhooks with signature verification and idempotency, writing contract tests for external services, or isolating third-party dependencies behind adapters. Triggers: \"Stripe\", \"payment integration\", \"webhook\", \"third-party API\", \"external service\", \"API client\", \"integrate with\", \"signature verification\", \"idempotency key\".

    Also in 3 projects that use this

The file

Show its rules and text

We split this skill into the rules it gives an agent

47 rules. 0 are common, 36 are distinctive, and 11 are very specific to this project.

  • 0

    Common

    Most of the 166 edited versions we read keep these.

  • 36

    Distinctive

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this guide to make REST APIs that are clear, safe, and easy to use.

    line 11
  2. Distinctive:

    Use this skill when you:

    line 15
  3. Distinctive:

    Add a new API route.

    line 17
  4. Distinctive:

    Add pages, filters, search, or sort.

    line 19
  5. Distinctive:

    Before you write code, list:

    line 26
  6. Distinctive:

    Keep names and response shapes the same across all routes.

    line 37
  7. Distinctive:

    Use plural nouns. Use lowercase words.

    line 43
  8. Distinctive:

    Use a nested URL when one resource belongs to another:

    line 54
  9. Distinctive:

    Keep nesting short. Do not go past two resource levels.

    line 61
  10. Distinctive:

    Use an action route only when normal create, read, update, and delete rules do not fit:

    line 67
  11. Very specific:

    Use one field style in JSON.

    line 90
  12. Very specific:

    Do not send a body with 204 No Content.

    line 142
  13. Very specific:

    Use either 400 or 422 for field checks.

    line 159
  14. Very specific:

    Add Retry-After to 429 and short-term 503 replies when you know when to retry.

    line 172
  15. Distinctive:

    Do not show stack traces, SQL, file paths, keys, tokens, or private data.

    line 174
  16. Distinctive:

    Use ISO 8601 dates in UTC:

    line 191
  17. Distinctive:

    Use strings for IDs. This avoids number size bugs in some tools.

    line 197
  18. Very specific:

    Keep code short and stable.

    line 244
  19. Very specific:

    Put field errors in details.

    line 246
  20. Distinctive:

    Add a request ID for support.

    line 247
  21. Distinctive:

    Never put secrets in an error.

    line 248
  22. Distinctive:

    Do not make clients read the message to learn the error type.

    line 249
  23. Very specific:

    Always use a fixed sort. Add a unique last sort field, such as id.

    line 272
  24. Distinctive:

    Use this for small lists and screens that need page numbers.

    line 296
  25. Distinctive:

    Read one extra row to learn if another page exists:

    line 304
  26. Distinctive:

    Use cursor pages for feeds, large lists, and public APIs.

    line 328
  27. Distinctive:

    Only allow known fields and actions.

    line 332
  28. Distinctive:

    Check these before work starts:

    line 379
  29. Very specific:

    Do not trust client fields such as user_id, role, price, or is_admin.

    line 392
  30. Distinctive:

    Use TLS for every API call.

    line 398
  31. Distinctive:

    Do not place keys or tokens in URLs.

    line 412
  32. Distinctive:

    Check both login and access:

    line 414
  33. Distinctive:

    Check access on every object.

    line 436
  34. Distinctive:

    When the limit is passed:

    line 468
  35. Distinctive:

    Use cache rules only when they are safe.

    line 498
  36. Very specific:

    Use Cache-Control: no-store for secrets and other private data that must not be saved.

    line 505
  37. Distinctive:

    Add all fields that change the reply to the cache key.

    line 507
  38. Distinctive:

    Never let one user receive another user's cached data.

    line 509
  39. Very specific:

    Start with /api/v1. Make a new major version only for a change that can break a client.

    line 520
  40. Distinctive:

    Add a new route.

    line 524
  41. Distinctive:

    Add an optional request field.

    line 525
  42. Distinctive:

    Add a response field, if clients are told to ignore unknown fields.

    line 526
  43. Distinctive:

    Add an optional query field.

    line 527
  44. Distinctive:

    Remove or rename a field.

    line 531
  45. Distinctive:

    Keep no more than two active major versions when you can.

    line 539
  46. Very specific:

    After the end date, return 410 Gone only if the route is truly removed.

    line 549
  47. Distinctive:

    Before you ship a route, check:

    line 709

“Common” is measured: kept by at least 6 in 10 of the 166 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

REST API Design

Original work by ECC. Credit to ECC for the ideas and base guide.

Use this guide to make REST APIs that are clear, safe, and easy to use.

When to Use This Skill

Use this skill when you:

  • Add a new API route.
  • Review an API contract.
  • Add pages, filters, search, or sort.
  • Add error handling.
  • Plan API versions.
  • Build a public or partner API.

Start With the Contract

Before you write code, list:

  1. The resource and URL.
  2. The request fields.
  3. The response fields.
  4. The status codes.
  5. The access rules.
  6. The page and sort rules.
  7. The rate limit.
  8. The errors a client may get.

Keep names and response shapes the same across all routes.

Resource Design

URL Rules

Use plural nouns. Use lowercase words. Join words with hyphens.

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

Use a nested URL when one resource belongs to another:

GET  /api/v1/users/:id/orders
POST /api/v1/users/:id/orders

Keep nesting short. Do not go past two resource levels. Use a top-level route if the child has its own ID:

GET /api/v1/orders/:id

Use an action route only when normal create, read, update, and delete rules do not fit:

POST /api/v1/orders/:id/cancel
POST /api/v1/auth/login
POST /api/v1/auth/refresh

Good and Bad Names

# Good
/api/v1/team-members
/api/v1/orders?status=active
/api/v1/users/123/orders

# Bad
/api/v1/getUsers
/api/v1/user
/api/v1/team_members
/api/v1/users/123/getOrders

Use one field style in JSON. This guide uses snake_case.

HTTP Methods

MethodSafe to retryChanges dataUse
GETYesNoRead data
POSTUsually noYesCreate data or run an action
PUTYesYesReplace all fields
PATCHIt dependsYesChange some fields
DELETEYesYesDelete data

A retry is safe only when the same call has the same final result. A PATCH that adds 1 each time is not safe. A PATCH that sets a name is safe.

For POST calls that may be retried, support an idempotency key:

Idempotency-Key: 7bb1c5c0-52a1-4f18-8af8-55ebf85e13fd

Store the key with the first result for a set time. Return that result for a repeat call.

Status Codes

Success

200 OK          GET, PUT, or PATCH with a body
201 Created     A new resource was made
202 Accepted    Work will finish later
204 No Content  Success with no body

For 201 Created, add a Location header:

HTTP/1.1 201 Created
Location: /api/v1/users/abc-123

For 202 Accepted, return a way to check the work:

{
  "data": {
    "job_id": "job-123",
    "status": "pending",
    "status_url": "/api/v1/jobs/job-123"
  }
}

Do not send a body with 204 No Content.

Client Errors

400 Bad Request           Bad JSON or bad request form
401 Unauthorized          Login data is missing or not valid
403 Forbidden             The caller is known but lacks access
404 Not Found             The resource does not exist
405 Method Not Allowed    The HTTP method is not allowed
409 Conflict              A duplicate or state clash happened
412 Precondition Failed   A write used an old version
415 Unsupported Media Type The body type is not allowed
422 Unprocessable Content The JSON is valid but its values are not
429 Too Many Requests     The rate limit was passed

Use either 400 or 422 for field checks. Pick one rule and use it on every route.

A private API may return 404 instead of 403 when this helps hide whether a resource exists.

Server Errors

500 Internal Server Error  An unknown server fault
502 Bad Gateway            A service used by this API failed
503 Service Unavailable    A short outage or heavy load
504 Gateway Timeout        A service used by this API took too long

Add Retry-After to 429 and short-term 503 replies when you know when to retry.

Do not show stack traces, SQL, file paths, keys, tokens, or private data.

Response Shapes

One Resource

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

Use ISO 8601 dates in UTC:

2025-01-15T10:30:00Z

Use strings for IDs. This avoids number size bugs in some tools.

A List

{
  "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"
  }
}

Return an empty list as []. Do not use null.

An Error

{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "request_id": "req-789",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "code": "invalid_format"
      }
    ]
  }
}

Rules for errors:

  • Keep code short and stable.
  • Make message safe for users.
  • Put field errors in details.
  • Add a request ID for support.
  • Never put secrets in an error.
  • Do not make clients read the message to learn the error type.

Choose one response style for the whole API:

interface ApiResponse<T> {
  data: T;
  meta?: PaginationMeta;
  links?: PaginationLinks;
}

interface ApiError {
  error: {
    code: string;
    message: string;
    request_id?: string;
    details?: FieldError[];
  };
}

Page Rules

Always use a fixed sort. Add a unique last sort field, such as id. Without this, rows may move or repeat.

Set a default and a hard limit:

Default limit: 20
Max limit: 100

Reject bad values, such as a negative page or a limit over the max.

Page and Offset

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

Use this for small lists and screens that need page numbers. Large offsets can be slow. New rows can also cause repeats or missed rows.

Cursor

GET /api/v1/users?cursor=opaque-value&limit=20

Read one extra row to learn if another page exists:

SELECT *
FROM users
WHERE (created_at, id) < (:cursor_created_at, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT 21;
{
  "data": [],
  "meta": {
    "has_next": true,
    "next_cursor": "opaque-value"
  }
}

The cursor must hold all sort fields. Sign or lock it so clients cannot change it. Treat it as a secret value, but do not place private data inside it.

Return 400 Bad Request for a bad or old cursor.

Use cursor pages for feeds, large lists, and public APIs. Cursor pages do not support a direct jump to page 50.

Filters, Sort, and Search

Only allow known fields and actions. Never place raw client input into SQL.

Filters

GET /api/v1/orders?status=active&customer_id=abc-123
GET /api/v1/products?price[gte]=10&price[lte]=100
GET /api/v1/orders?created_at[after]=2025-01-01
GET /api/v1/products?category=electronics,clothing
GET /api/v1/orders?customer.country=US

State how commas, spaces, empty values, and letter case work. URL encode all special signs.

Sort

GET /api/v1/products?sort=-created_at
GET /api/v1/products?sort=-featured,price,-created_at

A leading - means high to low. Add id as the last sort field when the client leaves it out.

Reject unknown sort fields.

Search

GET /api/v1/products?q=wireless%20headphones
GET /api/v1/users?email=alice

Set a max search length. State if search ignores letter case. Escape search text before use.

Pick Fields

GET /api/v1/users?fields=id,name,email
GET /api/v1/orders?fields=id,total,status&include=customer.name

Allow only safe fields. Do not let fields expose passwords, tokens, private notes, or hidden admin data.

Set a limit on linked data in include. This stops very large replies and slow calls.

Request Checks

Check these before work starts:

  • The body is valid JSON.
  • Content-Type is allowed.
  • Required fields exist.
  • Text and list sizes are within limits.
  • Values have the right type and range.
  • IDs have the right form.
  • Unknown fields follow one clear rule.
  • The caller has access to each resource.

Reject very large bodies before parsing them.

Do not trust client fields such as user_id, role, price, or is_admin. Get trusted values from the login data or server records.

For PATCH, state how null works. It may clear a field, or it may be banned. Do not leave this unclear.

Login and Access

Use TLS for every API call.

GET /api/v1/users
Authorization: Bearer token-value

For service calls:

GET /api/v1/data
X-API-Key: key-value

Do not place keys or tokens in URLs. URLs may be saved in logs and browser history.

Check both login and access:

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", message: "Order not found" }
    });
  }

  if (order.userId !== req.user.id) {
    return res.status(403).json({
      error: { code: "forbidden", message: "Access denied" }
    });
  }

  return res.json({ data: order });
});

Check access on every object. Do not rely only on a user ID from the URL.

Safe Updates

Two users may edit the same item at once. Use a version value or ETag to stop lost updates:

GET /api/v1/users/abc-123
ETag: "v7"
PATCH /api/v1/users/abc-123
If-Match: "v7"
Content-Type: application/json

Return 412 Precondition Failed if the item changed after the client read it.

For delete calls, decide if the item is fully removed or only marked as deleted. State this in the API guide.

Rate Limits

Rate limits must use a clear key, such as IP, user, API key, or service.

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

When the limit is passed:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Try again in 60 seconds."
  }
}

These are sample limits. Pick limits from real load tests and user needs:

GroupSample limitCount by
Guest30 each minuteIP
Signed in100 each minuteUser
Paid1,000 each minuteAPI key
Internal10,000 each minuteService

Set lower limits on costly routes, login tries, password reset, search, and file upload.

Caching

Use cache rules only when they are safe.

Cache-Control: private, max-age=60
ETag: "user-abc-123-v7"

Use Cache-Control: no-store for secrets and other private data that must not be saved.

Add all fields that change the reply to the cache key. This may include the user, query, language, and API version.

Never let one user receive another user's cached data.

API Versions

Path versions are clear and easy to test:

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

Start with /api/v1. Make a new major version only for a change that can break a client.

Changes that often do not need a new version:

  • Add a new route.
  • Add an optional request field.
  • Add a response field, if clients are told to ignore unknown fields.
  • Add an optional query field.

Changes that need a new version:

  • Remove or rename a field.
  • Change a field type.
  • Change a field from optional to required.
  • Change the meaning of a field.
  • Change a URL.
  • Change login rules.
  • Change the default sort in a way that breaks page use.

Keep no more than two active major versions when you can. Give public API users at least six months of notice when possible.

Send clear end dates:

Deprecation: true
Sunset: Thu, 01 Jan 2026 00:00:00 GMT
Link: <https://api.example.com/migrate-v2>; rel="deprecation"

After the end date, return 410 Gone only if the route is truly removed.

Concrete Example

Task: Add an endpoint that creates a user.

Contract:

POST /api/v1/users
Content-Type: application/json
Idempotency-Key: 7bb1c5c0-52a1-4f18-8af8-55ebf85e13fd

Request:

{
  "email": "alice@example.com",
  "name": "Alice"
}

Success:

HTTP/1.1 201 Created
Location: /api/v1/users/abc-123
Content-Type: application/json
{
  "data": {
    "id": "abc-123",
    "email": "alice@example.com",
    "name": "Alice",
    "created_at": "2025-01-15T10:30:00Z"
  }
}

Bad email:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "code": "invalid_format"
      }
    ]
  }
}

Email already used:

HTTP/1.1 409 Conflict
Content-Type: application/json
{
  "error": {
    "code": "email_in_use",
    "message": "That email address is already in use"
  }
}

TypeScript route:

import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";

const createUserSchema = z.object({
  email: z.string().email().max(254),
  name: z.string().trim().min(1).max(100),
}).strict();

export async function POST(req: NextRequest) {
  let body: unknown;

  try {
    body = await req.json();
  } catch {
    return NextResponse.json(
      {
        error: {
          code: "bad_json",
          message: "The request body must be valid JSON",
        },
      },
      { status: 400 }
    );
  }

  const parsed = createUserSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      {
        error: {
          code: "validation_error",
          message: "Request validation failed",
          details: parsed.error.issues.map((issue) => ({
            field: issue.path.join("."),
            message: issue.message,
            code: issue.code,
          })),
        },
      },
      { status: 422 }
    );
  }

  const found = await findUserByEmail(parsed.data.email);

  if (found) {
    return NextResponse.json(
      {
        error: {
          code: "email_in_use",
          message: "That email address is already in use",
        },
      },
      { status: 409 }
    );
  }

  const user = await createUser(parsed.data);

  return NextResponse.json(
    { data: user },
    {
      status: 201,
      headers: {
        Location: `/api/v1/users/${user.id}`,
      },
    }
  );
}

In real code, also add login checks, rate limits, body size limits, request IDs, and idempotency storage.

Review List

Before you ship a route, check:

  • The URL uses plural nouns.
  • The HTTP method fits the work.
  • Each result has the right status code.
  • Request fields have size and type checks.
  • Login and access checks are present.
  • Errors use one stable shape.
  • Secrets never appear in URLs or errors.
  • Lists have limits and a fixed sort.
  • Filters and sort fields use allowlists.
  • Writes handle retries when needed.
  • Updates guard against lost changes.
  • Rate limits are set.
  • Cache rules do not leak private data.
  • Breaking changes use a new version.
  • The API contract and examples are up to date.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.