engineering/api-design/skills/rest-api-design/SKILL.md
This skill should be used when the user asks about "REST API", "API design", "endpoint design", "HTTP methods", "status codes", "API versioning", "CRUD endpoints", "RESTful", "API conventions", "pagination", "cursor pagination", "offset pagination", "resource naming", "URL design", "API specification", "OpenAPI", "Swagger", "API contract", "request body", "response schema". Also trigger for "how should I design this API", "what status code should I use", "how to version an API", "is this good API design", "review my endpoint", or "how to structure my routes".
npx skillsauth add harsh040506/claude-code-unified-skill-plugin-library rest-api-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Production-quality patterns for designing, documenting, and evolving REST APIs.
Design around resources (nouns), not actions (verbs). Every endpoint operates on a resource.
✓ GET /orders — list orders
✓ POST /orders — create an order
✓ GET /orders/{id} — get a specific order
✓ PATCH /orders/{id} — update an order
✓ DELETE /orders/{id} — cancel/delete an order
✓ POST /orders/{id}/cancel — custom action (not a CRUD operation)
✗ GET /getOrders — verb in URL
✗ POST /cancelOrder — verb + noun, no resource hierarchy
✗ GET /orders/{id}/delete — wrong method for action
/v1/{resource} — collection
/v1/{resource}/{id} — single item
/v1/{resource}/{id}/{sub-resource} — nested (max 2 levels)
Naming:
- Plural nouns: /orders, /users, /invoices
- Kebab-case: /order-items, /shipping-addresses
- Lowercase always: /orders (not /Orders)
- No file extensions: /orders (not /orders.json)
- UUIDs for public IDs: /orders/b3d1f2a4-... (not /orders/12345)
| Method | Semantics | Idempotent | Safe | Body |
|--------|-----------|-----------|------|------|
| GET | Read | ✓ | ✓ | No |
| POST | Create / non-idempotent action | ✗ | ✗ | Yes |
| PUT | Full replace | ✓ | ✗ | Yes |
| PATCH | Partial update | ✓* | ✗ | Yes |
| DELETE | Remove | ✓ | ✗ | Sometimes |
| HEAD | GET without body | ✓ | ✓ | No |
| OPTIONS | Discover capabilities | ✓ | ✓ | No |
*PATCH is idempotent when it uses absolute values (not deltas).
200 OK — GET, PATCH, PUT success with body
201 Created — POST created a new resource; MUST include Location header
202 Accepted — Async operation started; not yet completed
204 No Content — DELETE success, or action with no meaningful response body
400 Bad Request — Malformed JSON, wrong data type, missing required field
401 Unauthorized — Not authenticated; include WWW-Authenticate header
403 Forbidden — Authenticated but not authorized
404 Not Found — Resource doesn't exist
405 Method Not Allowed — HTTP method not supported; include Allow header
409 Conflict — State conflict (duplicate, optimistic lock failure)
410 Gone — Resource permanently deleted (use 404 if uncertain)
422 Unprocessable — Valid syntax, business rule violation (e.g., order > account limit)
429 Too Many Requests — Rate limited; include Retry-After header
500 Internal Server Error — Never leak internal details in response body
502 Bad Gateway — Upstream dependency failed
503 Service Unavailable — Maintenance or overload; include Retry-After header
504 Gateway Timeout — Upstream timed out
✗ 200 + body { success: false } → use 4xx/5xx
✗ 200 for created resource → use 201 + Location header
✗ 400 for auth failure → use 401
✗ 404 for unauthorized → 403 (or 404 if you want to hide existence)
✗ 500 with stack trace in body → never expose internals
Always use the same error structure across your entire API.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
},
{
"field": "amount",
"code": "OUT_OF_RANGE",
"message": "Must be between 100 and 1000000 (cents)"
}
],
"request_id": "req_abc123",
"docs_url": "https://docs.example.com/errors#VALIDATION_FAILED"
}
}
Error code conventions:
NOT_FOUND, RATE_LIMITED, INSUFFICIENT_FUNDShttps://api.example.com/v1/orders
https://api.example.com/v2/orders
Advantages: obvious, easy to route, cacheable, loggable.
GET /orders HTTP/1.1
Accept: application/vnd.myapi+json;version=2
Advantages: clean URLs, follows HTTP spec. Harder to test in browser.
1. Release v2 endpoint
2. Announce v1 deprecation date (minimum 6 months for external APIs)
3. Add Deprecation: "Sat, 01 Jun 2024 00:00:00 GMT" header to v1 responses
4. Add Sunset: "Sat, 01 Dec 2024 00:00:00 GMT" header when sunset is scheduled
5. Retire v1 after sunset date
Breaking (requires version bump):
Non-breaking (backward-compatible):
// Request
GET /orders?limit=20&cursor=eyJpZCI6MTAwfQ
// Response
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"prev_cursor": "eyJpZCI6ODF9",
"has_next": true,
"has_prev": true,
"limit": 20
}
}
// Cursor implementation (encode last item's sort key)
function encodeCursor(item: Item): string {
return Buffer.from(JSON.stringify({ id: item.id })).toString('base64url');
}
function decodeCursor(cursor: string): { id: string } {
return JSON.parse(Buffer.from(cursor, 'base64url').toString());
}
// Usage in query
const decoded = decodeCursor(cursorParam);
const results = await db.query(
'SELECT * FROM orders WHERE id > $1 ORDER BY id LIMIT $2',
[decoded.id, limit + 1] // Fetch one extra to determine hasNext
);
const hasNext = results.length > limit;
// Request
GET /orders?page=3&per_page=20
// Response
{
"data": [...],
"pagination": {
"page": 3,
"per_page": 20,
"total_pages": 45,
"total_count": 892
}
}
Offset pagination has consistency issues with live data (items added/deleted between pages cause duplicates/skips). Use cursor for anything > 10k records or frequently updated.
openapi: "3.1.0"
info:
title: Orders API
version: "1.0.0"
description: |
Manage customer orders and their lifecycle.
All timestamps are ISO 8601 UTC. All monetary values are in cents (integer).
## Authentication
All endpoints require a Bearer token in the Authorization header.
## Rate Limits
100 requests per minute per API key. Limits returned in response headers.
servers:
- url: https://api.example.com/v1
description: Production
- url: https://staging.api.example.com/v1
description: Staging
security:
- bearerAuth: []
paths:
/orders:
get:
operationId: listOrders
summary: List orders
tags: [Orders]
parameters:
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/cursor'
- name: status
in: query
schema:
type: string
enum: [pending, processing, completed, cancelled]
responses:
'200':
description: Orders list
content:
application/json:
schema:
type: object
required: [data, pagination]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Order'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
post:
operationId: createOrder
summary: Create an order
tags: [Orders]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
headers:
Location:
schema:
type: string
example: /v1/orders/b3d1f2a4-c5d6-4e7f-8g9h-0i1j2k3l4m5n
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
$ref: '#/components/responses/ValidationError'
'422':
$ref: '#/components/responses/UnprocessableEntity'
/orders/{orderId}:
parameters:
- $ref: '#/components/parameters/orderId'
get:
operationId: getOrder
summary: Get an order
tags: [Orders]
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
Order:
type: object
required: [id, status, totalCents, createdAt, updatedAt]
properties:
id:
type: string
format: uuid
example: "b3d1f2a4-c5d6-4e7f-8g9h-0i1j2k3l4m5n"
status:
type: string
enum: [pending, processing, completed, cancelled]
totalCents:
type: integer
minimum: 0
example: 4999
createdAt:
type: string
format: date-time
example: "2024-01-15T14:30:00Z"
updatedAt:
type: string
format: date-time
CreateOrderRequest:
type: object
required: [items]
properties:
items:
type: array
minItems: 1
items:
type: object
required: [productId, quantity]
properties:
productId:
type: string
format: uuid
quantity:
type: integer
minimum: 1
Pagination:
type: object
properties:
nextCursor:
type: string
nullable: true
hasNext:
type: boolean
limit:
type: integer
Error:
type: object
required: [error]
properties:
error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
requestId:
type: string
details:
type: array
items:
type: object
parameters:
orderId:
name: orderId
in: path
required: true
schema:
type: string
format: uuid
limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
cursor:
name: cursor
in: query
schema:
type: string
responses:
Unauthorized:
description: Not authenticated
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ValidationError:
description: Request validation failed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
UnprocessableEntity:
description: Business rule violation
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
For payment, charge, and state-changing operations that must not run twice:
// Client: generate an idempotency key per operation attempt
const idempotencyKey = crypto.randomUUID(); // Same key on retry
await fetch('/v1/charges', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: 4999, currency: 'usd' }),
});
// Server: check if key was already processed
app.post('/v1/charges', async (req, res) => {
const key = req.headers['idempotency-key'];
if (!key) return res.status(400).json({ error: { code: 'MISSING_IDEMPOTENCY_KEY' } });
// Check cache / DB for existing result
const cached = await redis.get(`idem:${key}`);
if (cached) {
res.setHeader('Idempotent-Replayed', 'true');
return res.status(200).json(JSON.parse(cached));
}
// Process the charge
const result = await processCharge(req.body);
// Cache for 24h (long enough for any reasonable retry window)
await redis.setex(`idem:${key}`, 86400, JSON.stringify(result));
return res.status(201).json(result);
});
Full HATEOAS is complex and usually unnecessary. A pragmatic middle ground:
// Include links to related resources and allowed actions
{
"id": "order_abc",
"status": "pending",
"_links": {
"self": { "href": "/v1/orders/order_abc" },
"cancel": { "href": "/v1/orders/order_abc/cancel", "method": "POST" },
"items": { "href": "/v1/orders/order_abc/items" }
}
}
Only include links for actions the current user can take (omit cancel if order is already cancelled). This eliminates client-side "can I do this?" logic.
For complete OpenAPI specification templates and canonical error schema definitions, see:
references/openapi-patterns.md — full OpenAPI 3.1 spec templates for CRUD, pagination, bulk operations, and async job endpoints with reusable component schemasreferences/error-schemas.md — RFC 7807 Problem Details schema library, HTTP status decision guide, and error response examples for 30+ common API error scenariostesting
Performs quality control on single-cell RNA-seq data (.h5ad or .h5 files) using scverse best practices with MAD-based filtering and comprehensive visualizations. Use when users request QC analysis, filtering low-quality cells, assessing data quality, or following scverse/scanpy best practices for single-cell analysis.
tools
Deep learning for single-cell analysis using scvi-tools. This skill should be used when users need (1) data integration and batch correction with scVI/scANVI, (2) ATAC-seq analysis with PeakVI, (3) CITE-seq multi-modal analysis with totalVI, (4) multiome RNA+ATAC analysis with MultiVI, (5) spatial transcriptomics deconvolution with DestVI, (6) label transfer and reference mapping with scANVI/scArches, (7) RNA velocity with veloVI, or (8) any deep learning-based single-cell method. Triggers include mentions of scVI, scANVI, totalVI, PeakVI, MultiVI, DestVI, veloVI, sysVI, scArches, variational autoencoder, VAE, batch correction, data integration, multi-modal, CITE-seq, multiome, reference mapping, latent space.
testing
This skill should be used when scientists need help with research problem selection, project ideation, troubleshooting stuck projects, or strategic scientific decisions. Use this skill when users ask to pitch a new research idea, work through a project problem, evaluate project risks, plan research strategy, navigate decision trees, or get help choosing what scientific problem to work on. Typical requests include "I have an idea for a project", "I'm stuck on my research", "help me evaluate this project", "what should I work on", or "I need strategic advice about my research".
development
Run nf-core bioinformatics pipelines (rnaseq, sarek, atacseq) on sequencing data. Use when analyzing RNA-seq, WGS/WES, or ATAC-seq data—either local FASTQs or public datasets from GEO/SRA. Triggers on nf-core, Nextflow, FASTQ analysis, variant calling, gene expression, differential expression, GEO reanalysis, GSE/GSM/SRR accessions, or samplesheet creation.