Loading...
Loading...
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
npx skill4agent add petekp/claude-code-setup api-design-patterns/v1/usersAccept: application/vnd.api+json;version=1rest-patterns.mdgraphql-patterns.mdgrpc-patterns.mdversioning-strategies.mdauthentication.mdGET /users # List users
GET /users/123 # Get user
POST /users # Create user
PUT /users/123 # Update user (full)
PATCH /users/123 # Update user (partial)
DELETE /users/123 # Delete user
GET /users/123/orders # User's orders (sub-resource)GET /getUsers # Don't use verbs
POST /user/create # Don't use verbs
GET /Users/123 # Don't capitalize
GET /user/123 # Don't mix singular/plural200 OK201 Created202 Accepted204 No Content400 Bad Request401 Unauthorized403 Forbidden404 Not Found409 Conflict422 Unprocessable Entity429 Too Many Requests500 Internal Server Error502 Bad Gateway503 Service Unavailable504 Gateway Timeout{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
}
],
"request_id": "req_abc123",
"documentation_url": "https://api.example.com/docs/errors/validation"
}
}GET /users?limit=20&offset=40GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
Response: { "data": [...], "next_cursor": "eyJpZCI6MTQzfQ" }GET /users?limit=20&after_id=123references/rest-patterns.mdtype User {
id: ID! # Non-null ID
email: String! # Required field
name: String # Optional (nullable by default)
createdAt: DateTime!
orders: [Order!]! # Non-null array of non-null orders
}
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
input CreateUserInput {
email: String!
name: String
}
type CreateUserPayload {
user: User
userEdge: UserEdge
errors: [UserError!]
}import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await db.users.findMany({ where: { id: { in: userIds } } });
return userIds.map(id => users.find(u => u.id === id));
});
// Resolver batches queries automatically
const resolvers = {
Order: {
user: (order) => userLoader.load(order.userId)
}
};import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
schema,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query cost:', cost),
}),
],
});references/graphql-patterns.mdsyntax = "proto3";
package users.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (User) {}
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) {}
rpc CreateUser (CreateUserRequest) returns (User) {}
rpc StreamUsers (StreamUsersRequest) returns (stream User) {}
rpc BidiChat (stream ChatMessage) returns (stream ChatMessage) {}
}
message User {
string id = 1;
string email = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "user ID is required")
}
user, err := s.db.GetUser(ctx, req.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "database error")
}
return user, nil
}references/grpc-patterns.mdGET /v1/users/123
GET /v2/users/123GET /users/123
Accept: application/vnd.myapi.v2+jsonGET /users/123
Accept: application/vnd.myapi.user.v2+json{
"version": "1.0",
"deprecated": true,
"sunset_date": "2025-12-31",
"migration_guide": "https://docs.api.com/v1-to-v2",
"replacement_version": "2.0"
}HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: <https://docs.api.com/v1-to-v2>; rel="deprecation"references/versioning-strategies.md1. Client redirects to /authorize
2. User authenticates, grants permissions
3. Auth server redirects to callback with code
4. Client exchanges code for access token
5. Client uses access token for API requests# Request token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://client.com/callback
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET
# Response
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
# Use token
GET /v1/users/me
Authorization: Bearer eyJhbGc...{
"sub": "user_123",
"iat": 1516239022,
"exp": 1516242622,
"scope": "read:users write:orders"
}import jwt from 'jsonwebtoken';
const token = req.headers.authorization?.split(' ')[1];
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.userId = payload.sub;GET /v1/users
X-API-Key: sk_live_abc123...
# Or query parameter (less secure)
GET /v1/users?api_key=sk_live_abc123sk_live_sk_test_references/authentication.mdBucket: 100 tokens, refill 10/second
Request costs 1 token
Allows bursts up to bucket sizeHTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640995200HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"limit": 100,
"reset_at": "2025-01-01T00:00:00Z"
}
}POST /v1/payments
Idempotency-Key: uuid-or-client-generated-key
Content-Type: application/json
{
"amount": 1000,
"currency": "USD",
"customer": "cust_123"
}const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) {
return res.status(cached.status).json(cached.body);
}
}
const result = await processPayment(req.body);
await redis.setex(`idempotency:${idempotencyKey}`, 86400, {
status: 201,
body: result
});# Get resource with ETag
GET /v1/users/123
Response: ETag: "abc123"
# Update only if unchanged
PUT /v1/users/123
If-Match: "abc123"
# 412 Precondition Failed if ETag changed# Public, cacheable for 1 hour
Cache-Control: public, max-age=3600
# Private (user-specific), revalidate
Cache-Control: private, must-revalidate, max-age=0
# No caching
Cache-Control: no-store, no-cache, must-revalidate# Server returns ETag
GET /v1/users/123
Response:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=3600
# Client conditional request
GET /v1/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 304 Not Modified if unchanged (saves bandwidth)
HTTP/1.1 304 Not ModifiedGET /v1/users/123
Response:
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
# Conditional request
GET /v1/users/123
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
# 304 Not Modified if not modifiedPOST https://client.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Id: evt_abc123
X-Webhook-Timestamp: 1640995200
{
"id": "evt_abc123",
"type": "payment.succeeded",
"created": 1640995200,
"data": {
"object": {
"id": "pay_123",
"amount": 1000,
"status": "succeeded"
}
}
}import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
components:
schemas:
User:
type: object
required: [id, email]
properties:
id:
type: string
email:
type: string
format: email
name:
type: string"""
Represents a user account in the system.
Created via the createUser mutation.
"""
type User {
"""Unique identifier for the user"""
id: ID!
"""Email address, must be unique"""
email: String!
"""Optional display name"""
name: String
}?fields=id,name,email?expand=orders,profile// Pact contract test
import { PactV3 } from '@pact-foundation/pact';
const provider = new PactV3({
consumer: 'FrontendApp',
provider: 'UserAPI'
});
it('gets a user by ID', () => {
provider
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123'
})
.willRespondWith({
status: 200,
body: { id: '123', email: 'user@example.com' }
});
});// k6 load test
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 20 },
{ duration: '10s', target: 0 }
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'] // <1% errors
}
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500
});
}