Install
$ agentstack add skill-marvinrichter-clarc-api-design ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
API Design Patterns
Conventions and best practices for designing consistent, developer-friendly REST APIs.
When to Activate
- Before writing any implementation — design the contract first, code second
- Designing new API endpoints or changing existing ones
- Reviewing existing API contracts for consistency
- Adding pagination, filtering, or sorting — see skill
api-pagination-filtering - Implementing error handling for APIs
- Planning API versioning strategy
- Building public or partner-facing APIs
> For the full Contract-First workflow (spec writing, code generation, CI breaking-change detection, Pact): see skill api-contract. > For API documentation production — platform choice (Mintlify, Docusaurus, Redoc, Scalar), OpenAPI descriptions/examples, interactive playground, changelog automation, Vale prose linting, and Divio structure: see skill api-docs-patterns.
Contract-First Principle
Write the OpenAPI spec before writing any implementation code.
@startuml
:Write spec\napi/v1/openapi.yaml;
note right
REST → OpenAPI 3.1
Events → AsyncAPI 3.0
end note
:Lint spec\nspectral lint;
:Generate types / stubs\nfrom spec;
note right
TS: openapi-typescript
Go: oapi-codegen
Java: openapi-generator
Python: datamodel-codegen
end note
:Implement business logic only;
:Validate in CI\nschemathesis + oasdiff;
@enduml
The spec is the public contract. Consumers depend on it. Code is a private implementation detail.
- Never generate the spec from code (annotations, reflection) — it will drift
- Never write request/response types by hand — generate them from the spec
- Any breaking change requires a new API version (
/api/v2/)
See skill api-contract for the complete toolchain and CI setup.
Write descriptions and examples from the start
OpenAPI documentation is easiest to write while you are designing the spec — not after the implementation is shipped.
Minimum documentation requirements per operation (add these when you write each path, not later):
paths:
/orders:
post:
summary: Create an order # ← one-line summary
description: | # ← full description with scope, side effects, notes
Places a new order for the authenticated customer.
The order is created in `pending` status and transitions to
`processing` once payment is confirmed (async, webhook fired).
**Scopes required:** `orders:write`
operationId: createOrder # ← stable, unique identifier
tags: [Orders] # ← logical grouping
parameters: [] # ← every param needs description + example
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
example: # ← concrete, realistic example
customer_id: "cust_abc123"
items:
- product_id: "prod_xyz"
quantity: 2
responses:
'201':
description: Order created successfully.
headers:
Location:
description: URL of the newly created order.
schema: { type: string }
'400':
description: Request validation failed.
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
> See skill api-docs-patterns for the full documentation workflow: platform setup, interactive playground, code examples in all languages, changelog automation, and CI.
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 + RFC 7807 body
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/not-found",
"title": "Not Found",
"status": 404,
"detail": "User abc-123 not found.",
"instance": "/api/v1/users/abc-123"
}
# BAD: 500 for validation errors
# GOOD: 400 or 422 with field-level details (RFC 7807 errors extension)
# 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 — RFC 7807 / RFC 9457 Problem Details
All error responses MUST use Content-Type: application/problem+json and the standard fields:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-failed",
"title": "Validation Failed",
"status": 400,
"detail": "One or more fields failed validation.",
"instance": "/api/v1/users",
"errors": [
{ "field": "email", "detail": "must be a valid email address" },
{ "field": "age", "detail": "must be between 0 and 150" }
]
}
| Field | Required | Description | |---|---|---| | type | Recommended | URI identifying the problem type. about:blank if no docs exist yet. | | title | Recommended | Stable, human-readable summary (don't interpolate dynamic data). | | status | Yes | HTTP status code mirrored in the body. | | detail | Optional | Occurrence-specific explanation for the client. | | instance | Optional | URI of this specific occurrence (e.g., request path or ID). | | errors | Extension | RFC 9457 array for multiple sub-problems (validation errors). |
See skill: problem-details for full specification and per-language implementation.
Response Envelope Variants
// Success responses: return the resource (or data wrapper for public APIs)
interface ApiResponse {
data: T;
meta?: PaginationMeta;
links?: PaginationLinks;
}
// Error responses: always RFC 7807 ProblemDetails — never { success: false, error: "..." }
interface ProblemDetails {
type: string; // URI — link to docs
title: string; // Short, stable summary
status: number; // HTTP status mirrored
detail?: string; // Occurrence-specific detail
instance?: string; // URI of this occurrence
[key: string]: unknown; // Extension fields
}
// Content-Type for errors: application/problem+json (NOT application/json)
> For pagination (offset/cursor), filtering, sorting, and sparse fieldsets — see skill api-pagination-filtering.
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 — RFC 7807 body
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
{
"type": "https://api.example.com/problems/too-many-requests",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Try again in 60 seconds.",
"retryAfter": 60
}
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
For per-language implementation patterns (TypeScript, Go), anti-patterns, and the full API Design Checklist, see skill api-design-advanced.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: marvinrichter
- Source: marvinrichter/clarc
- License: MIT
- Homepage: https://marvinrichter.github.io/clarc
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.