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