rest-api-design

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

REST API Design

REST API 设计

Overview

概述

Design REST APIs that are intuitive, consistent, and follow industry best practices for resource-oriented architecture.
设计直观、一致且遵循面向资源架构行业最佳实践的REST API。

When to Use

适用场景

  • Designing new RESTful APIs
  • Creating endpoint structures
  • Defining request/response formats
  • Implementing API versioning
  • Documenting API specifications
  • Refactoring existing APIs
  • 设计新的RESTful API
  • 创建端点结构
  • 定义请求/响应格式
  • 实现API版本控制
  • 编写API规范文档
  • 重构现有API

Instructions

操作指南

1. Resource Naming

1. 资源命名

✅ Good Resource Names (Nouns, Plural)
GET    /api/users
GET    /api/users/123
GET    /api/users/123/orders
POST   /api/products
DELETE /api/products/456

❌ Bad Resource Names (Verbs, Inconsistent)
GET    /api/getUsers
POST   /api/createProduct
GET    /api/user/123  (inconsistent singular/plural)
✅ 良好的资源命名(名词、复数形式)
GET    /api/users
GET    /api/users/123
GET    /api/users/123/orders
POST   /api/products
DELETE /api/products/456

❌ 不佳的资源命名(动词、不一致)
GET    /api/getUsers
POST   /api/createProduct
GET    /api/user/123  (单复数形式不一致)

2. HTTP Methods & Operations

2. HTTP方法与操作

http
undefined
http
undefined

CRUD Operations

CRUD操作

GET /api/users # List all users (Read collection) GET /api/users/123 # Get specific user (Read single) POST /api/users # Create new user (Create) PUT /api/users/123 # Replace user completely (Update) PATCH /api/users/123 # Partial update user (Partial update) DELETE /api/users/123 # Delete user (Delete)
GET /api/users # 列出所有用户(读取集合) GET /api/users/123 # 获取指定用户(读取单个资源) POST /api/users # 创建新用户(创建) PUT /api/users/123 # 完整替换用户信息(全量更新) PATCH /api/users/123 # 部分更新用户信息(增量更新) DELETE /api/users/123 # 删除用户(删除)

Nested Resources

嵌套资源

GET /api/users/123/orders # Get user's orders POST /api/users/123/orders # Create order for user GET /api/users/123/orders/456 # Get specific order
undefined
GET /api/users/123/orders # 获取用户的订单列表 POST /api/users/123/orders # 为用户创建订单 GET /api/users/123/orders/456 # 获取指定订单
undefined

3. Request Examples

3. 请求示例

Creating a Resource

创建资源

http
POST /api/users
Content-Type: application/json

{
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "role": "admin"
}

Response: 201 Created
Location: /api/users/789
{
  "id": "789",
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "role": "admin",
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}
http
POST /api/users
Content-Type: application/json

{
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "role": "admin"
}

响应: 201 Created
Location: /api/users/789
{
  "id": "789",
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "role": "admin",
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}

Updating a Resource

更新资源

http
PATCH /api/users/789
Content-Type: application/json

{
  "firstName": "Jonathan"
}

Response: 200 OK
{
  "id": "789",
  "email": "john@example.com",
  "firstName": "Jonathan",
  "lastName": "Doe",
  "role": "admin",
  "updatedAt": "2025-01-15T11:00:00Z"
}
http
PATCH /api/users/789
Content-Type: application/json

{
  "firstName": "Jonathan"
}

响应: 200 OK
{
  "id": "789",
  "email": "john@example.com",
  "firstName": "Jonathan",
  "lastName": "Doe",
  "role": "admin",
  "updatedAt": "2025-01-15T11:00:00Z"
}

4. Query Parameters

4. 查询参数

http
undefined
http
undefined

Filtering

过滤

GET /api/products?category=electronics&inStock=true
GET /api/products?category=electronics&inStock=true

Sorting

排序

GET /api/users?sort=lastName,asc
GET /api/users?sort=lastName,asc

Pagination

分页

GET /api/users?page=2&limit=20
GET /api/users?page=2&limit=20

Field Selection

字段选择

GET /api/users?fields=id,email,firstName
GET /api/users?fields=id,email,firstName

Search

搜索

GET /api/products?q=laptop
GET /api/products?q=laptop

Multiple filters combined

组合多个过滤条件

GET /api/orders?status=pending&customer=123&sort=createdAt,desc&limit=50
undefined
GET /api/orders?status=pending&customer=123&sort=createdAt,desc&limit=50
undefined

5. Response Formats

5. 响应格式

Success Response

成功响应

json
{
  "data": {
    "id": "123",
    "email": "user@example.com",
    "firstName": "John"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "version": "1.0"
  }
}
json
{
  "data": {
    "id": "123",
    "email": "user@example.com",
    "firstName": "John"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "version": "1.0"
  }
}

Collection Response with Pagination

带分页的集合响应

json
{
  "data": [
    { "id": "1", "name": "Product 1" },
    { "id": "2", "name": "Product 2" }
  ],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 145,
    "totalPages": 8,
    "hasNext": true,
    "hasPrev": true
  },
  "links": {
    "self": "/api/products?page=2&limit=20",
    "first": "/api/products?page=1&limit=20",
    "prev": "/api/products?page=1&limit=20",
    "next": "/api/products?page=3&limit=20",
    "last": "/api/products?page=8&limit=20"
  }
}
json
{
  "data": [
    { "id": "1", "name": "Product 1" },
    { "id": "2", "name": "Product 2" }
  ],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 145,
    "totalPages": 8,
    "hasNext": true,
    "hasPrev": true
  },
  "links": {
    "self": "/api/products?page=2&limit=20",
    "first": "/api/products?page=1&limit=20",
    "prev": "/api/products?page=1&limit=20",
    "next": "/api/products?page=3&limit=20",
    "last": "/api/products?page=8&limit=20"
  }
}

Error Response

错误响应

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Email format is invalid"
      },
      {
        "field": "age",
        "message": "Must be at least 18"
      }
    ]
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "requestId": "abc-123-def"
  }
}
json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Email format is invalid"
      },
      {
        "field": "age",
        "message": "Must be at least 18"
      }
    ]
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "requestId": "abc-123-def"
  }
}

6. HTTP Status Codes

6. HTTP状态码

Success:
200 OK              - Successful GET, PATCH, DELETE
201 Created         - Successful POST (resource created)
204 No Content      - Successful DELETE (no response body)

Client Errors:
400 Bad Request     - Invalid request format/data
401 Unauthorized    - Missing or invalid authentication
403 Forbidden       - Authenticated but not authorized
404 Not Found       - Resource doesn't exist
409 Conflict        - Resource conflict (e.g., duplicate email)
422 Unprocessable   - Validation errors
429 Too Many Requests - Rate limit exceeded

Server Errors:
500 Internal Server Error - Generic server error
503 Service Unavailable   - Temporary unavailability
成功状态:
200 OK              - 成功的GET、PATCH、DELETE请求
201 Created         - 成功的POST请求(资源已创建)
204 No Content      - 成功的DELETE请求(无响应体)

客户端错误:
400 Bad Request     - 请求格式/数据无效
401 Unauthorized    - 缺少或无效的身份验证
403 Forbidden       - 已通过身份验证但无权限
404 Not Found       - 资源不存在
409 Conflict        - 资源冲突(例如:重复邮箱)
422 Unprocessable   - 验证错误
429 Too Many Requests - 请求次数超出限制

服务端错误:
500 Internal Server Error - 通用服务端错误
503 Service Unavailable   - 服务暂时不可用

7. API Versioning

7. API版本控制

http
undefined
http
undefined

URL Path Versioning (Recommended)

URL路径版本控制(推荐)

GET /api/v1/users GET /api/v2/users
GET /api/v1/users GET /api/v2/users

Header Versioning

请求头版本控制

GET /api/users Accept: application/vnd.myapi.v1+json
GET /api/users Accept: application/vnd.myapi.v1+json

Query Parameter (Not recommended)

查询参数版本控制(不推荐)

GET /api/users?version=1
undefined
GET /api/users?version=1
undefined

8. Authentication & Security

8. 身份验证与安全

http
undefined
http
undefined

JWT Bearer Token

JWT Bearer令牌

GET /api/users Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
GET /api/users Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

API Key

API密钥

GET /api/users X-API-Key: your-api-key-here
GET /api/users X-API-Key: your-api-key-here

Always use HTTPS in production

生产环境中始终使用HTTPS

9. Rate Limiting Headers

9. 请求频率限制响应头

http
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995
X-RateLimit-Reset: 1642262400
http
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995
X-RateLimit-Reset: 1642262400

10. OpenAPI Documentation

10. OpenAPI文档

yaml
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
  description: User management API

paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'

    post:
      summary: Create a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInput'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Invalid input
        '409':
          description: Email already exists

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
        createdAt:
          type: string
          format: date-time

    UserInput:
      type: object
      required:
        - email
        - firstName
        - lastName
      properties:
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
yaml
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
  description: User management API

paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'

    post:
      summary: Create a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInput'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Invalid input
        '409':
          description: Email already exists

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
        createdAt:
          type: string
          format: date-time

    UserInput:
      type: object
      required:
        - email
        - firstName
        - lastName
      properties:
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string

Best Practices

最佳实践

✅ DO

✅ 建议

  • Use nouns for resources, not verbs
  • Use plural names for collections
  • Be consistent with naming conventions
  • Return appropriate HTTP status codes
  • Include pagination for collections
  • Provide filtering and sorting options
  • Version your API
  • Document thoroughly with OpenAPI
  • Use HTTPS
  • Implement rate limiting
  • Provide clear error messages
  • Use ISO 8601 for dates
  • 使用名词而非动词命名资源
  • 集合资源使用复数名称
  • 保持命名约定一致
  • 返回合适的HTTP状态码
  • 为集合资源提供分页功能
  • 提供过滤和排序选项
  • 对API进行版本控制
  • 使用OpenAPI进行全面文档编写
  • 使用HTTPS
  • 实现请求频率限制
  • 提供清晰的错误信息
  • 日期使用ISO 8601格式

❌ DON'T

❌ 避免

  • Use verbs in endpoint names
  • Return 200 for errors
  • Expose internal IDs unnecessarily
  • Over-nest resources (max 2 levels)
  • Use inconsistent naming
  • Forget authentication
  • Return sensitive data
  • Break backward compatibility without versioning
  • 端点名称使用动词
  • 错误请求返回200状态码
  • 不必要地暴露内部ID
  • 资源过度嵌套(最多2层)
  • 命名不一致
  • 遗漏身份验证
  • 返回敏感数据
  • 不进行版本控制就破坏向后兼容性

Complete Example: Express.js

完整示例:Express.js

javascript
const express = require('express');
const app = express();

app.use(express.json());

// List users with pagination
app.get('/api/v1/users', async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const offset = (page - 1) * limit;

    const users = await User.findAndCountAll({
      limit,
      offset,
      attributes: ['id', 'email', 'firstName', 'lastName']
    });

    res.json({
      data: users.rows,
      pagination: {
        page,
        limit,
        total: users.count,
        totalPages: Math.ceil(users.count / limit)
      }
    });
  } catch (error) {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'An error occurred while fetching users'
      }
    });
  }
});

// Get single user
app.get('/api/v1/users/:id', async (req, res) => {
  try {
    const user = await User.findByPk(req.params.id);

    if (!user) {
      return res.status(404).json({
        error: {
          code: 'NOT_FOUND',
          message: 'User not found'
        }
      });
    }

    res.json({ data: user });
  } catch (error) {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'An error occurred'
      }
    });
  }
});

// Create user
app.post('/api/v1/users', async (req, res) => {
  try {
    const { email, firstName, lastName } = req.body;

    // Validation
    if (!email || !firstName || !lastName) {
      return res.status(400).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Missing required fields',
          details: [
            !email && { field: 'email', message: 'Email is required' },
            !firstName && { field: 'firstName', message: 'First name is required' },
            !lastName && { field: 'lastName', message: 'Last name is required' }
          ].filter(Boolean)
        }
      });
    }

    const user = await User.create({ email, firstName, lastName });

    res.status(201)
       .location(`/api/v1/users/${user.id}`)
       .json({ data: user });
  } catch (error) {
    if (error.name === 'SequelizeUniqueConstraintError') {
      return res.status(409).json({
        error: {
          code: 'CONFLICT',
          message: 'Email already exists'
        }
      });
    }
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'An error occurred'
      }
    });
  }
});

app.listen(3000);
javascript
const express = require('express');
const app = express();

app.use(express.json());

// 带分页的用户列表接口
app.get('/api/v1/users', async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const offset = (page - 1) * limit;

    const users = await User.findAndCountAll({
      limit,
      offset,
      attributes: ['id', 'email', 'firstName', 'lastName']
    });

    res.json({
      data: users.rows,
      pagination: {
        page,
        limit,
        total: users.count,
        totalPages: Math.ceil(users.count / limit)
      }
    });
  } catch (error) {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: '获取用户列表时发生错误'
      }
    });
  }
});

// 获取单个用户接口
app.get('/api/v1/users/:id', async (req, res) => {
  try {
    const user = await User.findByPk(req.params.id);

    if (!user) {
      return res.status(404).json({
        error: {
          code: 'NOT_FOUND',
          message: '用户不存在'
        }
      });
    }

    res.json({ data: user });
  } catch (error) {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: '发生错误'
      }
    });
  }
});

// 创建用户接口
app.post('/api/v1/users', async (req, res) => {
  try {
    const { email, firstName, lastName } = req.body;

    // 验证
    if (!email || !firstName || !lastName) {
      return res.status(400).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: '缺少必填字段',
          details: [
            !email && { field: 'email', message: '邮箱为必填项' },
            !firstName && { field: 'firstName', message: '名字为必填项' },
            !lastName && { field: 'lastName', message: '姓氏为必填项' }
          ].filter(Boolean)
        }
      });
    }

    const user = await User.create({ email, firstName, lastName });

    res.status(201)
       .location(`/api/v1/users/${user.id}`)
       .json({ data: user });
  } catch (error) {
    if (error.name === 'SequelizeUniqueConstraintError') {
      return res.status(409).json({
        error: {
          code: 'CONFLICT',
          message: '该邮箱已存在'
        }
      });
    }
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: '发生错误'
      }
    });
  }
});

app.listen(3000);