Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# api-design-patterns
|
||||||
|
|
||||||
|
API design patterns for REST, GraphQL, gRPC with versioning, security, and best practices
|
||||||
175
agents/api-architect.md
Normal file
175
agents/api-architect.md
Normal file
@@ -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
|
||||||
797
commands/api-patterns.md
Normal file
797
commands/api-patterns.md
Normal file
@@ -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<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**
|
||||||
49
plugin.lock.json
Normal file
49
plugin.lock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user