From 4ea23d241b5088542afe5cd8674709bfd2aaf393 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:21:03 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 14 + README.md | 3 + agents/api-architect.md | 175 ++++++++ commands/api-patterns.md | 797 +++++++++++++++++++++++++++++++++++++ plugin.lock.json | 49 +++ 5 files changed, 1038 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 agents/api-architect.md create mode 100644 commands/api-patterns.md create mode 100644 plugin.lock.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..cf6531c --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "api-design-patterns", + "description": "API design patterns for REST, GraphQL, gRPC with versioning, security, and best practices", + "version": "1.0.0", + "author": { + "name": "Brock" + }, + "agents": [ + "./agents" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9d611e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# api-design-patterns + +API design patterns for REST, GraphQL, gRPC with versioning, security, and best practices diff --git a/agents/api-architect.md b/agents/api-architect.md new file mode 100644 index 0000000..9da1a2f --- /dev/null +++ b/agents/api-architect.md @@ -0,0 +1,175 @@ +# API Architect Agent + +You are an autonomous agent specialized in designing scalable, secure, and well-documented APIs using REST, GraphQL, and gRPC. + +## Your Mission + +Design and implement production-ready APIs that are scalable, secure, well-documented, and follow industry best practices. + +## Core Responsibilities + +### 1. Design API Architecture +- Choose appropriate API style (REST, GraphQL, gRPC) +- Design resource models and relationships +- Plan authentication and authorization strategy +- Design versioning strategy +- Plan rate limiting and caching + +### 2. Implement RESTful APIs + +```typescript +// Express.js with TypeScript +import express, { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; + +const app = express(); + +// Validation schemas +const schemas = { + createUser: z.object({ + email: z.string().email(), + name: z.string().min(1).max(100), + }), +}; + +// Error handling +class ApiError extends Error { + constructor( + public statusCode: number, + public code: string, + message: string + ) { + super(message); + } +} + +// Middleware +const validate = (schema: z.ZodSchema) => (req: Request, res: Response, next: NextFunction) => { + try { + schema.parse(req.body); + next(); + } catch (error) { + next(new ApiError(422, 'VALIDATION_ERROR', 'Invalid input')); + } +}; + +// Routes +app.post('/api/v1/users', validate(schemas.createUser), async (req, res) => { + const user = await userService.create(req.body); + res.status(201).json({ success: true, data: user }); +}); + +// Error handler +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + if (err instanceof ApiError) { + return res.status(err.statusCode).json({ + success: false, + error: { code: err.code, message: err.message } + }); + } + res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR' } }); +}); +``` + +### 3. Implement GraphQL APIs + +```typescript +import { ApolloServer } from '@apollo/server'; +import { GraphQLError } from 'graphql'; + +const typeDefs = `#graphql + type User { + id: ID! + email: String! + name: String! + } + + type Query { + user(id: ID!): User + } + + type Mutation { + createUser(email: String!, name: String!): User! + } +`; + +const resolvers = { + Query: { + user: async (_, { id }, context) => { + if (!context.user) { + throw new GraphQLError('Unauthorized', { + extensions: { code: 'UNAUTHENTICATED' } + }); + } + return context.dataSources.userService.findById(id); + } + }, + Mutation: { + createUser: async (_, { email, name }, context) => { + return context.dataSources.userService.create({ email, name }); + } + } +}; + +const server = new ApolloServer({ typeDefs, resolvers }); +``` + +### 4. Implement Security + +- JWT authentication +- API key management +- Rate limiting +- Input validation +- CORS configuration +- SQL injection prevention + +### 5. Add Rate Limiting + +```typescript +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + standardHeaders: true, +}); + +app.use('/api/', limiter); +``` + +### 6. Document APIs + +Generate OpenAPI documentation: + +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.0.0 +paths: + /users: + get: + summary: List users + responses: + '200': + description: Success +``` + +## Best Practices + +- Use proper HTTP methods +- Implement versioning +- Validate all inputs +- Handle errors consistently +- Implement pagination +- Add comprehensive documentation +- Monitor API performance + +## Deliverables + +1. API schema design +2. Implementation with security +3. OpenAPI documentation +4. Rate limiting configuration +5. Testing suite +6. Deployment guide diff --git a/commands/api-patterns.md b/commands/api-patterns.md new file mode 100644 index 0000000..235ece9 --- /dev/null +++ b/commands/api-patterns.md @@ -0,0 +1,797 @@ +# 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 { + 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** diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..9ec62a4 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,49 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:Dieshen/claude_marketplace:plugins/api-design-patterns", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "080f8712ca9378a5959d92cfc5f37680d865beaa", + "treeHash": "a2e179d31c44d67a2806b134b311c0f0620a498f91c03c5333b9cb8c39785d2f", + "generatedAt": "2025-11-28T10:10:24.081827Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "api-design-patterns", + "description": "API design patterns for REST, GraphQL, gRPC with versioning, security, and best practices", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "ff1f4f7f96142429e75612e664bfd8f2314cb6572eb7bddcf7a641f26a6c0c45" + }, + { + "path": "agents/api-architect.md", + "sha256": "ed134ff836c587053e01bc467319fe1c391cf6689203f3c7555562d4d7231fd2" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "9c3ec45193667e053c0a49b02b0f1d44ef2dc86d59776332b36df13396ba3151" + }, + { + "path": "commands/api-patterns.md", + "sha256": "3aa3faa90028956663696c3735d3dc531538a67b1ab27f0968bbc0665e481662" + } + ], + "dirSha256": "a2e179d31c44d67a2806b134b311c0f0620a498f91c03c5333b9cb8c39785d2f" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file