| name | api-design |
| description | REST API, GraphQL, and API design patterns and best practices |
| license | MIT |
| compatibility | opencode |
API Design Skill
Comprehensive patterns and best practices for designing REST APIs, GraphQL APIs, and API-first development.
What I Know
REST API Design Principles
Resource Naming
# Good - nouns, plural, lowercase, hyphenated
GET /users
GET /users/{id}
GET /users/{id}/posts
GET /blog-posts
GET /order-items
# Bad - verbs, singular, camelCase
GET /getUsers
GET /user
GET /getUserPosts
GET /blogPosts
HTTP Methods
| Method | Usage | Idempotent | Request Body | Response |
|---|
| GET | Read resource(s) | Yes | No | Resource(s) |
| POST | Create resource | No | Yes | Created resource |
| PUT | Replace resource | Yes | Yes | Updated resource |
| PATCH | Partial update | No* | Yes | Updated resource |
| DELETE | Remove resource | Yes | No | Empty or confirmation |
Status Codes
# Success
200 OK - Successful GET, PUT, PATCH
201 Created - Successful POST
204 No Content - Successful DELETE
# Client Errors
400 Bad Request - Invalid request body/params
401 Unauthorized - Missing/invalid authentication
403 Forbidden - Valid auth, insufficient permissions
404 Not Found - Resource doesn't exist
409 Conflict - Resource state conflict (duplicate)
422 Unprocessable - Validation errors
# Server Errors
500 Internal Error - Unexpected server error
502 Bad Gateway - Upstream service error
503 Service Unavailable - Temporary overload
Standardized Response Format
Success Response
{
"success": true,
"data": {
"id": 123,
"email": "user@example.com",
"name": "John Doe"
}
}
List Response with Pagination
{
"success": true,
"data": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
],
"meta": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5,
"hasMore": true
}
}
Error Response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
]
}
}
Pagination Patterns
Offset-Based (Simple)
GET /users?page=2&limit=20
Response:
{
"data": [...],
"meta": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8
}
}
Cursor-Based (Scalable)
GET /users?cursor=abc123&limit=20
Response:
{
"data": [...],
"meta": {
"limit": 20,
"nextCursor": "def456",
"hasMore": true
}
}
Filtering & Sorting
Query Parameters
# Filtering
GET /users?status=active
GET /users?role=admin&status=active
GET /products?price_min=10&price_max=100
GET /posts?created_after=2024-01-01
# Sorting
GET /users?sort=name # Ascending
GET /users?sort=-createdAt # Descending
GET /users?sort=name,-createdAt # Multiple
# Field Selection
GET /users?fields=id,name,email
# Search
GET /users?search=john
GET /products?q=laptop
API Versioning
URI Versioning (Recommended)
GET /api/v1/users
GET /api/v2/users
Header Versioning
GET /api/users
Accept: application/vnd.api+json;version=1
Query Parameter
GET /api/users?version=1
Authentication Patterns
JWT Bearer Token
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
API Key
# Header
X-API-Key: your-api-key
# Query (less secure)
GET /api/users?api_key=your-api-key
OAuth 2.0 Flows
# Authorization Code (Web apps)
# Client Credentials (Server-to-server)
# PKCE (Mobile/SPA)
Rate Limiting
Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200
Retry-After: 60
429 Response
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"retryAfter": 60
}
}
OpenAPI/Swagger Specification
openapi: 3.0.3
info:
title: My API
version: 1.0.0
description: API for managing users
servers:
- url: https://api.example.com/v1
description: Production
paths:
/users:
get:
summary: List users
tags:
- Users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Success
[]
GraphQL Patterns
Schema Design
type Query {
user(id: ID!): User
users(
page: Int = 1
limit: Int = 20
filter: UserFilter
): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
deleteUser(id: ID!): DeletePayload!
}
type User {
id: ID!
String
String
posts Int Post
DateTime
CreateUserInput
String
String
String
UpdateUserInput
String
String
UserFilter
UserStatus
UserRole
String
UserConnection
UserEdge
PageInfo
Int
UserEdge
User
String
PageInfo
Boolean
Boolean
String
String
UserPayload
User
Error
DeletePayload
Boolean
Error
Error
String
String
Resolver Patterns
const resolvers = {
Query: {
user: async (_, { id }, context) => {
return context.dataSources.users.findById(id)
},
users: async (_, { page, limit, filter }, context) => {
return context.dataSources.users.findAll({ page, limit, filter })
},
},
Mutation: {
createUser: async (_, { input }, context) => {
try {
const user = await context.dataSources.users.create(input)
return { user, errors: [] }
} catch (error) {
return { user: null, errors: [{ message: error.message }] }
}
},
},
User: {
posts: async (user, { first }, context) => {
return context.dataSources.posts.findByUserId(user.id, { first })
},
},
}
Error Handling Best Practices
Error Codes
enum ErrorCode {
INVALID_TOKEN = 'INVALID_TOKEN',
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',
VALIDATION_ERROR = 'VALIDATION_ERROR',
INVALID_INPUT = 'INVALID_INPUT',
NOT_FOUND = 'NOT_FOUND',
ALREADY_EXISTS = 'ALREADY_EXISTS',
CONFLICT = 'CONFLICT',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
INTERNAL_ERROR = 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
}
HATEOAS (Hypermedia)
{
"success": true,
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"links": {
"self": "/api/v1/users/123",
"posts": "/api/v1/users/123/posts",
"update": "/api/v1/users/123",
"delete": "/api/v1/users/123"
}
}
Caching Headers
# Cache for 1 hour
Cache-Control: max-age=3600, public
# No cache
Cache-Control: no-store, no-cache, must-revalidate
# ETag for conditional requests
ETag: "abc123"
If-None-Match: "abc123"
# Last-Modified
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
Security Best Practices
- Always use HTTPS
- Validate all input - Never trust client data
- Rate limit endpoints - Prevent abuse
- Use proper authentication - JWT, OAuth 2.0
- Implement authorization - Check permissions
- Sanitize output - Prevent XSS
- Use parameterized queries - Prevent SQL injection
- Log security events - Audit trail
- Don't expose internals - Generic error messages
- Set security headers - CORS, CSP, etc.
API Documentation
Good Documentation Includes:
- Authentication guide
- Request/response examples
- Error code reference
- Rate limiting info
- Changelog/versioning
- SDKs and code samples
- Interactive playground (Swagger UI)
Common Pitfalls
- Inconsistent naming - Stick to one convention
- Missing validation - Always validate input
- No versioning - Version from the start
- Poor error messages - Be helpful, not cryptic
- No rate limiting - Protect from abuse
- Exposing internals - Don't leak stack traces
- Ignoring caching - Set proper cache headers
- No pagination - Always paginate lists
- Breaking changes - Use versioning properly
- Missing documentation - Document everything
Part of SuperAI GitHub - Centralized OpenCode Configuration