798 lines
17 KiB
Markdown
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**
|