Files
2025-11-29 18:21:03 +08:00

17 KiB

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

// 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

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

# 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)

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

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)

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

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

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

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

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