| name | api-dev |
| description | REST and GraphQL API design and development covering schema-first design, versioning, authentication, rate limiting, error handling, and documentation. This skill should be used when designing APIs, building REST or GraphQL backends, implementing authentication and rate limiting, or creating API documentation. |
API Development
This skill provides structured guidance for designing and building robust REST and GraphQL APIs, with patterns for schema design, versioning, authentication, rate limiting, and documentation.
Prerequisites
- A backend framework (Express, Fastify, Django, FastAPI, etc.)
- Understanding of HTTP fundamentals
- Database access for persistence
When to Use This Skill
- Designing a new REST or GraphQL API
- Implementing authentication and authorization
- Adding rate limiting and request throttling
- Versioning an existing API
- Creating API documentation (OpenAPI/GraphQL schema)
- Structuring error responses consistently
REST API Design
Schema-First Design Process
- Define resources — What entities does your API expose?
- Define relationships — How do resources relate to each other?
- Define operations — What can clients do with each resource?
- Define representations — What does each resource look like in JSON?
- Define status codes — What HTTP status for each outcome?
URL Structure
# ✅ Resource-based URLs (nouns, not verbs)
GET /api/v1/users # List users
POST /api/v1/users # Create user
GET /api/v1/users/:id # Get user
PATCH /api/v1/users/:id # Update user
DELETE /api/v1/users/:id # Delete user
# ✅ Nested resources (limit to 1 level of nesting)
GET /api/v1/users/:id/posts # User's posts
POST /api/v1/users/:id/posts # Create post for user
# ✅ Actions as sub-resources (when not CRUD)
POST /api/v1/users/:id/activate # Activate user
POST /api/v1/orders/:id/cancel # Cancel order
# ❌ Avoid verb-based URLs
GET /api/v1/getUsers
POST /api/v1/createUser
POST /api/v1/deleteUser/123
Response Format
{
"data": {
"id": "usr_abc123",
"name": "Jane Doe",
"email": "jane@example.com"
},
"meta": {
"request_id": "req_xyz789",
"timestamp": "2025-01-15T10:30:00Z"
}
}
{
"data": [
{ "id": "usr_abc123", "name": "Jane Doe" }
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 156,
"total_pages": 8
},
"links": {
"self": "/api/v1/users?page=1",
"next": "/api/v1/users?page=2",
"last": "/api/v1/users?page=8"
}
}
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
}
],
"request_id": "req_xyz789"
}
}
HTTP Status Codes
| Code | Meaning | When to Use |
|---|
| 200 | OK | Successful GET, PATCH, DELETE |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE with no body |
| 400 | Bad Request | Validation failure, malformed input |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate resource, state conflict |
| 422 | Unprocessable Entity | Semantic validation failure |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server failure |
Filtering, Sorting, Pagination
# Filtering (field=value, supports operators)
GET /api/v1/users?status=active&created_after=2025-01-01
# Sorting (field for asc, -field for desc)
GET /api/v1/users?sort=-created_at,name
# Pagination (cursor-based for large datasets)
GET /api/v1/users?cursor=abc123&limit=20
# Pagination (offset-based for small datasets)
GET /api/v1/users?page=2&per_page=20
# Field selection (sparse fieldsets)
GET /api/v1/users?fields=id,name,email
# Include related resources
GET /api/v1/users/:id?include=posts,profile
GraphQL API Design
Schema-First Approach
type User {
id: ID!
name: String!
email: String!
role: Role!
posts(limit: Int = 10): [Post!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [Tag!]!
publishedAt: DateTime
}
enum Role {
ADMIN
EDITOR
VIEWER
}
type Query {
user(id: ID!): User
users(
filter: UserFilter
sort: UserSort
first: Int = 20
after: String
): UserConnection!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
}
type Subscription {
userUpdated(id: ID): User!
postCreated(authorId: ID): Post!
}
input CreateUserInput {
name: String!
email: String!
role: Role = VIEWER
}
type CreateUserPayload {
user: User
errors: [FieldError!]
}
type FieldError {
field: String!
message: String!
code: String!
}
GraphQL Best Practices
- Separate input and output types — CreateUserInput ≠ User
- Use cursor-based pagination —
first/after pattern with Connection types
- N+1 prevention — Use DataLoader for batched fetching
- Nullability — Mark fields
! only when they're guaranteed to exist
- Versioning — Evolve schema (add fields, never remove); avoid versioned endpoints
- Depth limiting — Cap query depth to prevent abuse (max 5-7 levels)
- Cost analysis — Assign complexity scores and limit per-query cost
Authentication
JWT Pattern
interface TokenPayload {
sub: string;
iat: number;
exp: number;
role: string;
scope: string[];
}
async function authMiddleware(req, res, next) {
const token = extractBearerToken(req);
if (!token) {
return res.status(401).json({
error: { code: 'UNAUTHORIZED', message: 'Missing authorization token' }
});
}
try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: { code: 'TOKEN_EXPIRED', message: 'Token has expired' }
});
}
return res.status(401).json({
error: { code: 'INVALID_TOKEN', message: 'Token is invalid' }
});
}
}
function requireRole(...roles: string[]) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' }
});
}
next();
};
}
API Key Pattern
interface ApiKey {
key: string;
prefix: string;
scopes: string[];
rate_limit: number;
expires_at: Date;
}
Rate Limiting
Implementation Strategies
async function rateLimiter(
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
const now = Date.now();
const windowKey = `ratelimit:${key}`;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(windowKey, 0, now - windowMs);
pipeline.zadd(windowKey, now, `${now}-${Math.random()}`);
pipeline.zcard(windowKey);
pipeline.pexpire(windowKey, windowMs);
const results = await pipeline.exec();
const count = results[2][1] as number;
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
resetAt: now + windowMs,
};
}
Rate Limit Headers
X-RateLimit-Limit: 100 # Max requests per window
X-RateLimit-Remaining: 67 # Remaining in current window
X-RateLimit-Reset: 1704067200 # Unix timestamp when window resets
Retry-After: 30 # Seconds to wait (only on 429)
Rate Limit Tiers
| Tier | Limit | Use Case |
|---|
| Anonymous | 30 req/min | Unauthenticated exploration |
| Authenticated | 100 req/min | Normal user operations |
| Premium | 300 req/min | Power users, internal services |
| Service | 1000 req/min | Backend service-to-service |
Versioning
Strategies Comparison
| Strategy | URL Pattern | Pros | Cons |
|---|
| URL path | /api/v1/users | Explicit, cacheable | URL changes |
| Header | Accept: application/vnd.api.v1+json | Clean URLs | Hidden in headers |
| Query param | /api/users?version=1 | Simple | Not RESTful, caching issues |
Recommendation: URL path versioning for REST, schema evolution for GraphQL.
Versioning Workflow
- New version — Create new route prefix or endpoint
- Deprecation notice — Add
Sunset and Deprecation headers to old version
- Migration period — Support both versions for 6+ months
- Communication — Email, changelog, header notices
- Sunset — Remove old version, return 410 Gone
Error Handling
Error Code System
const ErrorCodes = {
AUTH_UNAUTHORIZED: 'Authentication required',
AUTH_TOKEN_EXPIRED: 'Token has expired',
AUTH_FORBIDDEN: 'Insufficient permissions',
VAL_INVALID_INPUT: 'Request validation failed',
VAL_MISSING_FIELD: 'Required field is missing',
VAL_INVALID_FORMAT: 'Field format is invalid',
RES_NOT_FOUND: 'Resource not found',
RES_CONFLICT: 'Resource already exists',
RES_GONE: 'Resource has been removed',
RATE_LIMITED: 'Rate limit exceeded',
} as const;
class ApiError extends Error {
constructor(
public code: string,
public status: number,
message: string,
public details?: unknown[]
) {
super(message);
}
}
Tips
- Use schema-first design for both REST and GraphQL — define the contract before implementing
- Return consistent error envelopes with machine-readable codes, not just messages
- Paginate all list endpoints; use cursor-based pagination for large datasets
- Version your REST API in the URL path; evolve your GraphQL schema instead of versioning
- Rate limit by default, even for authenticated users — abuse comes from all sources
- Document with OpenAPI (REST) or introspection + descriptions (GraphQL) — never keep docs separate from code
- Use
Sunset headers when deprecating endpoints — give clients time to migrate
- Log request IDs for tracing — every error response should include the request ID
Cline Workflow Notes
- Install location: Copy this skill directory to
.cline/skills/api-dev/ (project-level) or ~/.cline/skills/api-dev/ (global)
- Activation: Cline will automatically suggest this skill when you mention API design, REST endpoints, or GraphQL schemas
- Progressive loading: Only the metadata loads initially; full instructions activate via
use_skill when API work begins