Files
gh-dieshen-claude-marketpla…/commands/api-patterns.md
2025-11-29 18:21:03 +08:00

798 lines
17 KiB
Markdown

# API Design Patterns
Comprehensive API design patterns for REST, GraphQL, gRPC with security, versioning, and best practices.
## RESTful API Design
### Resource-Based URL Design
```
Good URL Design:
GET /api/v1/users - List users
GET /api/v1/users/{id} - Get specific user
POST /api/v1/users - Create user
PUT /api/v1/users/{id} - Update user (full)
PATCH /api/v1/users/{id} - Update user (partial)
DELETE /api/v1/users/{id} - Delete user
GET /api/v1/users/{id}/posts - Get user's posts
POST /api/v1/users/{id}/posts - Create post for user
Bad URL Design:
GET /api/v1/getAllUsers
POST /api/v1/createNewUser
GET /api/v1/user-posts/{id}
```
### HTTP Status Codes
```
Success:
200 OK - Successful GET, PUT, PATCH, DELETE
201 Created - Successful POST
204 No Content - Successful DELETE with no response body
Client Errors:
400 Bad Request - Invalid request data
401 Unauthorized - Missing or invalid authentication
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
409 Conflict - Resource conflict (duplicate)
422 Unprocessable - Validation errors
429 Too Many Requests - Rate limit exceeded
Server Errors:
500 Internal Server - Server error
502 Bad Gateway - Upstream error
503 Service Unavailable - Temporary unavailability
```
### Request/Response Format
```typescript
// Consistent response structure
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: any;
};
meta?: {
timestamp: string;
requestId: string;
};
}
// Success response
{
"success": true,
"data": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
},
"meta": {
"timestamp": "2024-01-01T12:00:00Z",
"requestId": "req_abc123"
}
}
// Error response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"email": ["Email is required", "Email format is invalid"]
}
},
"meta": {
"timestamp": "2024-01-01T12:00:00Z",
"requestId": "req_abc123"
}
}
// Paginated list response
{
"success": true,
"data": {
"items": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"totalPages": 5,
"totalItems": 95,
"hasNext": true,
"hasPrevious": false
}
}
}
```
### Node.js/Express Implementation
```typescript
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
// Validation schema
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
// Middleware: Request validation
const validate = (schema: z.ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(422).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: error.errors,
},
});
}
next(error);
}
};
};
// Middleware: Error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err);
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
success: false,
error: {
code: err.code,
message: err.message,
},
meta: {
timestamp: new Date().toISOString(),
requestId: req.id,
},
});
}
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
});
// Routes
app.get('/api/v1/users', async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const pageSize = parseInt(req.query.pageSize as string) || 20;
const result = await userService.list({ page, pageSize });
res.json({
success: true,
data: {
items: result.items,
pagination: {
page,
pageSize,
totalPages: Math.ceil(result.total / pageSize),
totalItems: result.total,
hasNext: page * pageSize < result.total,
hasPrevious: page > 1,
},
},
});
});
app.get('/api/v1/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError('User not found');
}
res.json({
success: true,
data: user,
});
});
app.post('/api/v1/users', validate(userSchema), async (req, res) => {
const user = await userService.create(req.body);
res.status(201).json({
success: true,
data: user,
});
});
app.patch('/api/v1/users/:id', async (req, res) => {
const user = await userService.update(req.params.id, req.body);
res.json({
success: true,
data: user,
});
});
app.delete('/api/v1/users/:id', async (req, res) => {
await userService.delete(req.params.id);
res.status(204).send();
});
```
## GraphQL API Design
### Schema Definition
```graphql
# schema.graphql
type User {
id: ID!
email: String!
name: String!
posts(first: Int, after: String): PostConnection!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
post(id: ID!): Post
posts(first: Int, after: String): PostConnection!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
}
input CreateUserInput {
email: String!
name: String!
}
input UpdateUserInput {
email: String
name: String
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
scalar DateTime
```
### Resolver Implementation (TypeScript)
```typescript
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
user: async (_parent, { id }, context) => {
if (!context.user) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
const user = await context.dataSources.userService.findById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: { code: 'NOT_FOUND' },
});
}
return user;
},
users: async (_parent, { first = 20, after }, context) => {
const result = await context.dataSources.userService.list({
first,
after,
});
return {
edges: result.items.map(item => ({
cursor: item.cursor,
node: item,
})),
pageInfo: {
hasNextPage: result.hasNextPage,
hasPreviousPage: result.hasPreviousPage,
startCursor: result.startCursor,
endCursor: result.endCursor,
},
totalCount: result.totalCount,
};
},
},
Mutation: {
createUser: async (_parent, { input }, context) => {
if (!context.user) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
try {
const user = await context.dataSources.userService.create(input);
return user;
} catch (error) {
if (error.code === 'DUPLICATE_EMAIL') {
throw new GraphQLError('Email already exists', {
extensions: { code: 'BAD_USER_INPUT' },
});
}
throw error;
}
},
},
User: {
posts: async (parent, { first, after }, context) => {
return context.dataSources.postService.listByUser(parent.id, {
first,
after,
});
},
},
Post: {
author: async (parent, _args, context) => {
return context.dataSources.userService.findById(parent.authorId);
},
},
};
// DataLoader for N+1 prevention
import DataLoader from 'dataloader';
const createLoaders = (dataSources) => ({
userLoader: new DataLoader(async (userIds) => {
const users = await dataSources.userService.findByIds(userIds);
return userIds.map(id => users.find(user => user.id === id));
}),
});
```
## gRPC API Design
### Protocol Buffer Definition
```protobuf
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (User);
rpc UpdateUser(UpdateUserRequest) returns (User);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
rpc StreamUsers(StreamUsersRequest) returns (stream User);
}
message User {
string id = 1;
string email = 2;
string name = 3;
int64 created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
bool has_next = 3;
}
message CreateUserRequest {
string email = 1;
string name = 2;
}
message UpdateUserRequest {
string id = 1;
optional string email = 2;
optional string name = 3;
}
message DeleteUserRequest {
string id = 1;
}
message DeleteUserResponse {
bool success = 1;
}
message StreamUsersRequest {
// Empty for now
}
```
### gRPC Server (Go)
```go
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "myapp/proto/user/v1"
)
type server struct {
pb.UnimplementedUserServiceServer
userService UserService
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.userService.FindById(ctx, req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "internal error")
}
return &pb.User{
Id: user.ID,
Email: user.Email,
Name: user.Name,
CreatedAt: user.CreatedAt.Unix(),
}, nil
}
func (s *server) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
result, err := s.userService.List(ctx, page, pageSize)
if err != nil {
return nil, status.Error(codes.Internal, "internal error")
}
users := make([]*pb.User, len(result.Items))
for i, u := range result.Items {
users[i] = &pb.User{
Id: u.ID,
Email: u.Email,
Name: u.Name,
CreatedAt: u.CreatedAt.Unix(),
}
}
return &pb.ListUsersResponse{
Users: users,
Total: int32(result.Total),
HasNext: result.HasNext,
}, nil
}
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
if req.Email == "" {
return nil, status.Error(codes.InvalidArgument, "email is required")
}
if req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
user, err := s.userService.Create(ctx, CreateUserInput{
Email: req.Email,
Name: req.Name,
})
if err != nil {
return nil, status.Error(codes.Internal, "internal error")
}
return &pb.User{
Id: user.ID,
Email: user.Email,
Name: user.Name,
CreatedAt: user.CreatedAt.Unix(),
}, nil
}
func (s *server) StreamUsers(req *pb.StreamUsersRequest, stream pb.UserService_StreamUsersServer) error {
users, err := s.userService.FindAll(stream.Context())
if err != nil {
return status.Error(codes.Internal, "internal error")
}
for _, user := range users {
if err := stream.Send(&pb.User{
Id: user.ID,
Email: user.Email,
Name: user.Name,
CreatedAt: user.CreatedAt.Unix(),
}); err != nil {
return err
}
}
return nil
}
```
## API Versioning Strategies
### URL Path Versioning
```
/api/v1/users
/api/v2/users
```
### Header Versioning
```
GET /api/users
Accept: application/vnd.myapp.v2+json
```
### Query Parameter Versioning
```
/api/users?version=2
```
## API Security
### JWT Authentication
```typescript
import jwt from 'jsonwebtoken';
interface JwtPayload {
userId: string;
email: string;
}
const authMiddleware = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
error: { code: 'UNAUTHORIZED', message: 'Authentication required' },
});
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload;
req.user = await userService.findById(payload.userId);
next();
} catch (error) {
return res.status(401).json({
success: false,
error: { code: 'INVALID_TOKEN', message: 'Invalid or expired token' },
});
}
};
```
### Rate Limiting
```typescript
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
const limiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:',
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests, please try again later',
},
},
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
```
### API Key Authentication
```typescript
const apiKeyAuth = async (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({
success: false,
error: { code: 'API_KEY_REQUIRED', message: 'API key is required' },
});
}
const key = await apiKeyService.validate(apiKey);
if (!key || !key.isActive) {
return res.status(401).json({
success: false,
error: { code: 'INVALID_API_KEY', message: 'Invalid API key' },
});
}
req.apiKey = key;
next();
};
```
## API Documentation with OpenAPI
```yaml
openapi: 3.0.0
info:
title: My API
version: 1.0.0
description: API for managing users and posts
servers:
- url: https://api.example.com/v1
description: Production server
paths:
/users:
get:
summary: List users
parameters:
- in: query
name: page
schema:
type: integer
description: Page number
- in: query
name: pageSize
schema:
type: integer
description: Number of items per page
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserInput'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
name:
type: string
CreateUserInput:
type: object
required:
- email
- name
properties:
email:
type: string
name:
type: string
Pagination:
type: object
properties:
page:
type: integer
totalPages:
type: integer
```
## Best Practices
1. **Use proper HTTP methods and status codes**
2. **Version your APIs**
3. **Implement pagination for list endpoints**
4. **Use consistent response formats**
5. **Validate all inputs**
6. **Implement rate limiting**
7. **Secure with authentication and authorization**
8. **Document your API**
9. **Monitor and log API usage**
10. **Handle errors gracefully**