Opinionated REST API design guidance for resource naming, HTTP semantics, error formats, pagination, versioning, and more. Use when designing REST APIs during brainstorming or plan creation.
From tommymorgannpx claudepluginhub tommymorgan/claude-plugins --plugin tommymorganThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Provides security patterns for LLM trading agents with wallet/transaction authority: prompt injection guards, spend limits, pre-send simulation, circuit breakers, MEV protection, key handling.
Opinionated guidance for designing consistent, developer-friendly REST APIs. Apply these guidelines when designing new APIs or reviewing existing API designs.
Pattern: /{serviceRoot}/{collection}/{id}
URLs should be human-readable and hierarchical:
https://api.example.com/v1/people/jdoe@example.com/inboxhttps://api.example.com/v1/orders/12345/itemsRules:
/users, /orders, /messages)URL length: Design for under 2,083 characters to accommodate all clients.
| Method | Purpose | Idempotent | Key Rules |
|---|---|---|---|
| GET | Retrieve resource | Yes | Safe, cacheable, no side effects |
| POST | Create resource or submit command | No | Return 201 Created with Location header |
| PUT | Replace entire resource | Yes | Full replacement — unspecified properties are removed |
| PATCH | Partial update | No | Preferred over PUT for updates; supports UPSERT |
| DELETE | Remove resource | Yes | Return 204 No Content |
| HEAD | Metadata only (no body) | Yes | Same as GET without response body |
| OPTIONS | Discover capabilities | Yes | Return Allow header listing supported methods |
PATCH for UPSERT:
If-Match: update only (409 if missing)If-None-Match: *: create only (409 if exists)| Code | Meaning | Use When |
|---|---|---|
| 200 | OK | Standard success with response body |
| 201 | Created | Resource created; include Location header |
| 202 | Accepted | Long-running operation initiated |
| 204 | No Content | Success with no response body (DELETE, some updates) |
| Code | Meaning | Use When |
|---|---|---|
| 400 | Bad Request | Invalid request syntax or parameters |
| 401 | Unauthorized | Authentication required or failed |
| 403 | Forbidden | Authenticated but lacks permission |
| 404 | Not Found | Resource does not exist |
| 405 | Method Not Allowed | HTTP method not supported on this resource |
| 409 | Conflict | Request conflicts with current state |
| 429 | Too Many Requests | Rate limit or quota exceeded |
| Code | Meaning | Use When |
|---|---|---|
| 500 | Internal Server Error | Unhandled server failure |
| 503 | Service Unavailable | Server overloaded or in maintenance |
Important: Rate-limit failures (429) are client errors, not faults. Only 5xx errors count as faults affecting availability.
All error responses use a single JSON object with a mandatory error property:
{
"error": {
"code": "BadArgument",
"message": "Human-readable description for developers",
"target": "propertyName",
"details": [],
"innererror": {}
}
}
| Property | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Language-independent, stable error code (~20 possible values) |
message | string | Yes | Developer-facing description; do not localize; not for end-user display |
target | string | No | What caused the error (e.g., property name) |
details | Error[] | No | Array of related errors, each with code and message |
innererror | object | No | More specific nested error detail |
{
"error": {
"code": "BadArgument",
"message": "Previous passwords may not be reused",
"target": "password",
"innererror": {
"code": "PasswordError",
"innererror": {
"code": "PasswordDoesNotMeetPolicy",
"minLength": "6",
"maxLength": "64"
}
}
}
}
{
"error": {
"code": "BadArgument",
"message": "Multiple validation errors",
"details": [
{ "code": "NullValue", "target": "phoneNumber", "message": "Phone number is required" },
{ "code": "NullValue", "target": "lastName", "message": "Last name is required" }
]
}
}
Rules:
code values is a breaking changeinnererror for new granular codes without breaking clientsinnererror chain and act on the deepest understood codeRetry-After header for transient errorsWrap collections in a value property:
{
"value": [
{ "id": "1", "name": "Item One" },
{ "id": "2", "name": "Item Two" }
]
}
Empty collections return 200 OK with empty array:
{
"value": []
}
Include a continuation token as @nextLink (an opaque URL):
{
"value": [ ... ],
"@nextLink": "https://api.example.com/v1/people?$skip=20"
}
@nextLink indicates the final page| Parameter | Purpose |
|---|---|
$top | Maximum number of items to return |
$skip | Number of items to skip |
Server applies $skip first, then $top. The server must return an error if it cannot honor the parameters (never silently ignore them).
Clients may specify $maxpagesize as a preferred page size. Servers should honor it if smaller than the server default.
Use $count=true to include total item count across all pages.
Parameter: $filter
Boolean expression evaluated for each resource. Only items where the expression is true are included.
| Category | Operator | Example |
|---|---|---|
| Comparison | eq | city eq 'Portland' |
ne | city ne 'Portland' | |
gt | price gt 20 | |
ge | price ge 10 | |
lt | price lt 20 | |
le | price le 100 | |
| Logical | and | price le 200 and price gt 3.5 |
or | price le 3.5 or price gt 200 | |
not | not price le 3.5 | |
| Grouping | ( ) | (status eq 'active' or status eq 'pending') and price gt 100 |
( ) — Groupingnot — Unarygt, ge, lt, le — Relationaleq, ne — Equalityand — Conditional ANDor — Conditional ORParameter: $orderBy
| Syntax | Example |
|---|---|
| Property name | $orderBy=name |
| Descending | $orderBy=name desc |
| Multiple properties | $orderBy=name desc,createdDate |
Default direction is ascending. NULL values sort before non-NULL values.
Sorting composes with filtering: GET /people?$filter=status eq 'active'&$orderBy=name
If the server doesn't support sorting by a requested property, return an error.
All APIs must support explicit versioning.
Format: Major.Minor (e.g., 1.0, 2.1)
Two mechanisms:
https://api.example.com/v1/productshttps://api.example.com/products?api-version=1.0When to version:
Breaking changes include:
Documentation: indicate support status of each previous version and the path to the latest.
Use camelCase for all JSON property names:
firstName, lastName, dateOfBirthfirst_name, FirstName, FIRST_NAME| Name | Purpose |
|---|---|
id | Unique identifier |
name | Display or primary name |
description | Textual description |
createdDateTime | Creation timestamp |
lastModifiedDateTime | Last modification timestamp |
status | Current state |
type | Resource type classification |
Use ISO 8601 format: 2025-02-13T13:15:00Z
Suffixes: Date, Time, DateTime, Timestamp
createdDate, lastModifiedTime, expirationDateTimeaddresses, orders)count suffix (addressCount)emailAddress not email_addr)All APIs should support CORS.
For every request with an Origin header:
Access-Control-Allow-Origin echoing the request's Origin valueAccess-Control-Expose-Headers listing non-simple response headersAccess-Control-Allow-Credentials: true if cookies are requiredFor OPTIONS preflight requests, additionally include:
Access-Control-Allow-Headers: permitted request headersAccess-Control-Allow-Methods: permitted HTTP methodsAccess-Control-Max-Age: how long the preflight response is cached (in seconds)Return 200 OK for preflight requests with no additional processing.
Authorization strategy: enforce authorization through valid tokens, not origin validation.
| Code | Use When |
|---|---|
| 429 | Client rate limit or quota exceeded |
| 503 | Server overload protection (fast-fail) |
| Header | Purpose |
|---|---|
Retry-After | Seconds to wait before retrying |
RateLimit-Limit | Quota window size (e.g., requests per hour) |
RateLimit-Remaining | Remaining quota in current window |
RateLimit-Reset | Seconds until quota resets |
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 1000
RateLimit-Remaining: 0
RateLimit-Reset: 1800
Retry-After headerClients create subscriptions for specific resource changes:
POST https://api.example.com/v1/subscriptions
Content-Type: application/json
{
"notificationUrl": "https://client.example.com/webhook",
"resource": "/users/123/messages",
"changeType": "created,updated,deleted",
"clientState": "client-context-value"
}
| Operation | Method | Endpoint |
|---|---|---|
| Create | POST | /subscriptions |
| Update | PATCH | /subscriptions/{id} |
| Delete | DELETE | /subscriptions/{id} |
| List | GET | /subscriptions |
When a subscription is created:
notificationUrl with validationToken query parametertext/plain)This prevents malicious subscription creation to third-party endpoints.
{
"value": [
{
"subscriptionId": "sub-123",
"clientState": "client-context-value",
"changeType": "created",
"resource": "/users/123/messages/456",
"resourceData": {
"id": "456"
},
"sequenceNumber": 1
}
]
}
X-Webhook-Signature: sha256=...)clientState provides additional verification that the notification originated from the expected serviceFor operations expected to take more than 0.5 seconds (99th percentile):
Operation-Location headernotStarted, running, succeeded, failed{
"createdDateTime": "2025-02-13T12:01:03Z",
"lastActionDateTime": "2025-02-13T12:06:03Z",
"status": "running",
"percentComplete": 45,
"resourceLocation": "https://api.example.com/v1/exports/export-789"
}
Include Retry-After header to indicate polling interval.