pin_drop当前位置:知识文库 ❯ 图文

后端通信:RESTful API - 从入门到实践详解

教程简介

REST(Representational State Transfer)是当今最流行的Web API设计架构风格,几乎所有的现代Web应用都采用RESTful API进行前后端通信。理解REST的设计原则、资源与URI设计、HTTP方法语义、状态码选择、分页排序过滤、版本管理等知识,不仅对前端开发者调用API至关重要,也是全栈开发的核心技能。

本教程将全面讲解REST架构原则、资源与URI设计、HTTP方法语义映射、状态码选择、分页排序过滤、版本管理、HATEOAS以及RESTful最佳实践。

核心概念

1. REST架构原则

REST由Roy Fielding在2000年提出,包含六个核心约束:

约束 说明
客户端-服务器 前后端分离,独立演进
无状态 每个请求包含所有必要信息
可缓存 响应必须明确标识是否可缓存
统一接口 一致的资源操作方式
分层系统 中间层(代理、网关)对客户端透明
按需代码(可选) 服务器可以返回可执行代码

统一接口是REST最重要的约束,包含四个子约束:

  1. 资源标识:通过URI标识资源

  2. 通过表示操作资源:通过资源的表示(JSON/XML)操作资源

  3. 自描述消息:每条消息包含足够的信息来描述如何处理

  4. 超媒体作为应用状态引擎(HATEOAS):响应包含相关操作的链接

2. 资源与URI设计

REST的核心概念是资源(Resource),URI是资源的唯一标识。

URI设计原则

代码示例

// 好的设计 - 名词表示资源
GET /api/users              // 用户集合
GET /api/users/1            // 单个用户
GET /api/users/1/orders     // 用户的订单
GET /api/orders/101/items   // 订单中的商品

// 不好的设计 - 动词表示操作
GET /api/getUsers
POST /api/createUser
PUT /api/updateUser/1
DELETE /api/deleteUser/1

URI设计规范

规则 示例 说明
使用名词复数 /users, /orders 资源用复数
使用小写字母 /user-profiles 避免驼峰
用连字符分隔 /user-profiles 不用下划线
层级表示关系 /users/1/orders 子资源
查询参数过滤 /users?role=admin 过滤条件
避免深层嵌套 /orders/101/items/5 不超过3层

3. HTTP方法语义映射

操作 HTTP方法 URI 说明
获取列表 GET /users 返回用户列表
获取单个 GET /users/1 返回ID为1的用户
创建 POST /users 创建新用户
完整替换 PUT /users/1 替换ID为1的用户
部分更新 PATCH /users/1 更新ID为1的用户部分字段
删除 DELETE /users/1 删除ID为1的用户

非CRUD操作的处理

代码示例

// 方式1:将操作视为资源
POST /users/1/activation     // 激活用户
POST /orders/101/cancellation // 取消订单

// 方式2:使用子资源
POST /users/1/password-reset  // 重置密码
POST /files/5/compression     // 压缩文件

// 方式3:使用查询参数
POST /emails?action=send      // 发送邮件

4. 状态码选择

操作 成功状态码 失败状态码
GET列表 200 OK -
GET单个 200 OK 404 Not Found
POST创建 201 Created 400 Bad Request, 409 Conflict
PUT替换 200 OK 400 Bad Request, 404 Not Found, 409 Conflict
PATCH更新 200 OK 400 Bad Request, 404 Not Found, 422 Unprocessable
DELETE删除 204 No Content 404 Not Found

5. 分页、排序、过滤

代码示例

// 分页
GET /api/users?page=2&size=20

// 排序
GET /api/users?sort=created_at&order=desc

// 过滤
GET /api/users?role=admin&status=active

// 搜索
GET /api/users?q=张三

// 字段选择
GET /api/users?fields=id,name,email

// 组合使用
GET /api/users?role=admin&sort=created_at&order=desc&page=1&size=20

分页响应格式

代码示例

{
    "data": [
        { "id": 1, "name": "张三" },
        { "id": 2, "name": "李四" }
    ],
    "pagination": {
        "page": 1,
        "size": 20,
        "total": 156,
        "totalPages": 8
    },
    "links": {
        "self": "/api/users?page=1&size=20",
        "next": "/api/users?page=2&size=20",
        "prev": null,
        "first": "/api/users?page=1&size=20",
        "last": "/api/users?page=8&size=20"
    }
}

6. 版本管理

代码示例

// 方式1:URL路径(最常用)
GET /api/v1/users
GET /api/v2/users

// 方式2:请求头
GET /api/users
Accept: application/vnd.myapi.v2+json

// 方式3:查询参数
GET /api/users?version=2

URL路径版本化的优缺点:

优点 缺点
简单直观 URL变化
易于缓存 版本间可能重复代码
浏览器直接访问 不符合REST纯粹主义

7. HATEOAS

HATEOAS(Hypermedia as the Engine of Application State)要求API响应包含相关操作的链接:

代码示例

{
    "id": 1,
    "name": "张三",
    "email": "zhangsan@example.com",
    "_links": {
        "self": { "href": "/api/v1/users/1" },
        "orders": { "href": "/api/v1/users/1/orders" },
        "update": { "href": "/api/v1/users/1", "method": "PUT" },
        "delete": { "href": "/api/v1/users/1", "method": "DELETE" }
    }
}

8. RESTful最佳实践

请求格式

代码示例

// POST /api/users - 创建用户
{
    "name": "张三",
    "email": "zhangsan@example.com",
    "age": 25
}

响应格式

代码示例

// 成功响应
{
    "code": 0,
    "message": "success",
    "data": {
        "id": 1,
        "name": "张三",
        "email": "zhangsan@example.com",
        "created_at": "2026-04-22T10:30:00Z"
    }
}

// 错误响应
{
    "code": 40001,
    "message": "参数验证失败",
    "errors": [
        { "field": "email", "message": "邮箱格式不正确" },
        { "field": "age", "message": "年龄必须大于0" }
    ]
}

代码示例

示例1:RESTful API设计器

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RESTful API设计器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 1000px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .resource-list { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
        .resource-btn { padding: 8px 16px; border: 2px solid #e8eaed; border-radius: 8px; background: #fff; cursor: pointer; font-weight: 600; font-size: 13px; transition: all 0.2s; }
        .resource-btn.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
        .endpoint-table { width: 100%; border-collapse: collapse; }
        .endpoint-table th, .endpoint-table td { padding: 12px 14px; text-align: left; border-bottom: 1px solid #e8eaed; font-size: 14px; }
        .endpoint-table th { background: #f8f9fa; font-weight: 600; color: #555; }
        .method-badge { display: inline-block; padding: 3px 10px; border-radius: 4px; font-weight: 700; font-size: 12px; color: #fff; }
        .method-get { background: #34a853; }
        .method-post { background: #ea4335; }
        .method-put { background: #1a73e8; }
        .method-patch { background: #ff9800; }
        .method-delete { background: #9c27b0; }
        .uri-text { font-family: 'Consolas', monospace; font-size: 13px; color: #555; }
        .try-btn { padding: 4px 12px; background: #1a73e8; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; }
        .response-area { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.7; max-height: 300px; overflow-y: auto; white-space: pre-wrap; margin-top: 16px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>RESTful API设计器</h1>
        <div class="card">
            <h2>选择资源</h2>
            <div class="resource-list">
                <div class="resource-btn active" onclick="selectResource('users')">Users 用户</div>
                <div class="resource-btn" onclick="selectResource('orders')">Orders 订单</div>
                <div class="resource-btn" onclick="selectResource('products')">Products 产品</div>
                <div class="resource-btn" onclick="selectResource('articles')">Articles 文章</div>
            </div>
        </div>
        <div class="card">
            <h2>API端点</h2>
            <table class="endpoint-table" id="endpointTable"></table>
        </div>
        <div class="card">
            <h2>模拟响应</h2>
            <div class="response-area" id="responseArea">点击端点表格中的"试一试"查看模拟响应</div>
        </div>
    </div>
    <script>
        const resources = {
            users: {
                name: '用户', fields: ['id', 'name', 'email', 'role', 'status', 'created_at'],
                endpoints: [
                    { method: 'GET', uri: '/api/v1/users', desc: '获取用户列表', status: 200, response: { code: 0, message: 'success', data: [{id:1,name:'张三',email:'zhangsan@example.com',role:'admin',status:'active',created_at:'2026-04-22T10:30:00Z'},{id:2,name:'李四',email:'lisi@example.com',role:'user',status:'active',created_at:'2026-04-21T08:00:00Z'}], pagination: {page:1,size:20,total:156,totalPages:8} }},
                    { method: 'GET', uri: '/api/v1/users/1', desc: '获取单个用户', status: 200, response: { code: 0, message: 'success', data: {id:1,name:'张三',email:'zhangsan@example.com',role:'admin',status:'active',created_at:'2026-04-22T10:30:00Z'} }},
                    { method: 'POST', uri: '/api/v1/users', desc: '创建用户', status: 201, response: { code: 0, message: '创建成功', data: {id:3,name:'王五',email:'wangwu@example.com',role:'user',status:'active',created_at:'2026-04-22T12:00:00Z'} }},
                    { method: 'PUT', uri: '/api/v1/users/1', desc: '替换用户', status: 200, response: { code: 0, message: '更新成功', data: {id:1,name:'张三',email:'new@example.com',role:'admin',status:'active',created_at:'2026-04-22T10:30:00Z',updated_at:'2026-04-22T14:00:00Z'} }},
                    { method: 'PATCH', uri: '/api/v1/users/1', desc: '部分更新', status: 200, response: { code: 0, message: '更新成功', data: {id:1,email:'new@example.com'} }},
                    { method: 'DELETE', uri: '/api/v1/users/1', desc: '删除用户', status: 204, response: null },
                    { method: 'GET', uri: '/api/v1/users?role=admin&status=active', desc: '过滤查询', status: 200, response: { code: 0, message: 'success', data: [{id:1,name:'张三',role:'admin',status:'active'}] }},
                    { method: 'GET', uri: '/api/v1/users?sort=created_at&order=desc&page=1&size=10', desc: '分页排序', status: 200, response: { code: 0, data: [], pagination: {page:1,size:10,total:156,totalPages:16} }}
                ]
            },
            orders: {
                name: '订单', fields: ['id', 'user_id', 'total', 'status', 'items', 'created_at'],
                endpoints: [
                    { method: 'GET', uri: '/api/v1/orders', desc: '获取订单列表', status: 200, response: { code: 0, data: [{id:101,user_id:1,total:299.9,status:'paid',created_at:'2026-04-22T10:00:00Z'}] }},
                    { method: 'GET', uri: '/api/v1/orders/101', desc: '获取订单详情', status: 200, response: { code: 0, data: {id:101,user_id:1,total:299.9,status:'paid',items:[{product_id:1,quantity:2,price:149.95}],created_at:'2026-04-22T10:00:00Z'} }},
                    { method: 'POST', uri: '/api/v1/orders', desc: '创建订单', status: 201, response: { code: 0, message: '创建成功', data: {id:102,user_id:1,total:599.8,status:'pending'} }},
                    { method: 'POST', uri: '/api/v1/orders/101/cancellation', desc: '取消订单', status: 200, response: { code: 0, message: '订单已取消', data: {id:101,status:'cancelled'} }},
                    { method: 'GET', uri: '/api/v1/users/1/orders', desc: '获取用户的订单', status: 200, response: { code: 0, data: [{id:101,total:299.9,status:'paid'}] }}
                ]
            },
            products: {
                name: '产品', fields: ['id', 'name', 'price', 'category', 'stock'],
                endpoints: [
                    { method: 'GET', uri: '/api/v1/products', desc: '获取产品列表', status: 200, response: { code: 0, data: [{id:1,name:'商品A',price:99.9,category:'电子',stock:100}] }},
                    { method: 'GET', uri: '/api/v1/products/1', desc: '获取产品详情', status: 200, response: { code: 0, data: {id:1,name:'商品A',price:99.9,category:'电子',stock:100,description:'优质商品'} }},
                    { method: 'POST', uri: '/api/v1/products', desc: '创建产品', status: 201, response: { code: 0, data: {id:2,name:'商品B',price:199.9} }},
                    { method: 'PATCH', uri: '/api/v1/products/1/stock', desc: '更新库存', status: 200, response: { code: 0, data: {id:1,stock:95} }}
                ]
            },
            articles: {
                name: '文章', fields: ['id', 'title', 'content', 'author', 'tags', 'published_at'],
                endpoints: [
                    { method: 'GET', uri: '/api/v1/articles', desc: '获取文章列表', status: 200, response: { code: 0, data: [{id:1,title:'RESTful入门',author:'张三',published_at:'2026-04-22'}] }},
                    { method: 'GET', uri: '/api/v1/articles/1', desc: '获取文章详情', status: 200, response: { code: 0, data: {id:1,title:'RESTful入门',content:'...',author:'张三',tags:['REST','API']} }},
                    { method: 'POST', uri: '/api/v1/articles', desc: '创建文章', status: 201, response: { code: 0, data: {id:2,title:'新文章'} }},
                    { method: 'POST', uri: '/api/v1/articles/1/publish', desc: '发布文章', status: 200, response: { code: 0, data: {id:1,status:'published',published_at:'2026-04-22T12:00:00Z'} }},
                    { method: 'GET', uri: '/api/v1/articles?q=REST', desc: '搜索文章', status: 200, response: { code: 0, data: [{id:1,title:'RESTful入门'}] }}
                ]
            }
        };

        let currentResource = 'users';

        function selectResource(name) {
            currentResource = name;
            document.querySelectorAll('.resource-btn').forEach(b => b.classList.remove('active'));
            event.currentTarget.classList.add('active');
            renderEndpoints();
        }

        function renderEndpoints() {
            const resource = resources[currentResource];
            document.getElementById('endpointTable').innerHTML = `
                <thead><tr><th>方法</th><th>URI</th><th>说明</th><th>状态码</th><th>操作</th></tr></thead>
                <tbody>
                ${resource.endpoints.map((ep, i) => `
                    <tr>
                        <td><span class="method-badge method-${ep.method.toLowerCase()}">${ep.method}</span></td>
                        <td class="uri-text">${ep.uri}</td>
                        <td>${ep.desc}</td>
                        <td>${ep.status}</td>
                        <td><button class="try-btn" onclick="tryEndpoint('${currentResource}', ${i})">试一试</button></td>
                    </tr>
                `).join('')}
                </tbody>
            `;
        }

        function tryEndpoint(resource, index) {
            const ep = resources[resource].endpoints[index];
            let output = `${ep.method} ${ep.uri}\nHTTP/1.1 ${ep.status}\nContent-Type: application/json\n\n`;
            if (ep.response) {
                output += JSON.stringify(ep.response, null, 2);
            } else {
                output += '(无响应体)';
            }
            document.getElementById('responseArea').textContent = output;
        }

        renderEndpoints();
    </script>
</body>
</html>

示例2:RESTful客户端模拟

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RESTful客户端模拟</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 1000px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .full-width { grid-column: 1 / -1; }
        .crud-bar { display: flex; gap: 8px; margin-bottom: 16px; }
        .crud-btn { padding: 8px 16px; border: none; border-radius: 6px; color: #fff; font-weight: 700; font-size: 13px; cursor: pointer; }
        .btn-get { background: #34a853; }
        .btn-post { background: #ea4335; }
        .btn-put { background: #1a73e8; }
        .btn-delete { background: #9c27b0; }
        .user-table { width: 100%; border-collapse: collapse; }
        .user-table th, .user-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e8eaed; font-size: 13px; }
        .user-table th { background: #f8f9fa; font-weight: 600; }
        .user-table tr:hover { background: #f8f9fa; }
        .action-btn { padding: 4px 8px; border: none; border-radius: 4px; font-size: 11px; cursor: pointer; color: #fff; font-weight: 600; }
        .form-group { margin-bottom: 10px; }
        .form-group label { display: block; font-size: 13px; font-weight: 600; color: #555; margin-bottom: 4px; }
        .form-group input { width: 100%; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 6px; font-size: 14px; }
        .log-area { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 12px; line-height: 1.8; max-height: 300px; overflow-y: auto; white-space: pre-wrap; }
        .log-method { font-weight: 700; }
        .log-get { color: #4caf50; } .log-post { color: #f44336; } .log-put { color: #2196f3; } .log-delete { color: #9c27b0; }
        .log-uri { color: #ffd54f; } .log-status { color: #81c784; }
    </style>
</head>
<body>
    <div class="container">
        <h1>RESTful客户端模拟</h1>
        <div class="main-grid">
            <div class="card">
                <h2>用户资源</h2>
                <div class="crud-bar">
                    <button class="crud-btn btn-get" onclick="listUsers()">GET /users</button>
                    <button class="crud-btn btn-post" onclick="showCreate()">POST /users</button>
                </div>
                <table class="user-table">
                    <thead><tr><th>ID</th><th>姓名</th><th>邮箱</th><th>操作</th></tr></thead>
                    <tbody id="userTableBody"><tr><td colspan="4" style="text-align:center;color:#999;">点击GET加载</td></tr></tbody>
                </table>
            </div>
            <div class="card">
                <h2>操作表单</h2>
                <div id="formArea"><p style="color:#888;text-align:center;padding:30px;">选择操作</p></div>
            </div>
            <div class="card full-width">
                <h2>请求日志</h2>
                <div class="log-area" id="logArea">等待操作...</div>
            </div>
        </div>
    </div>
    <script>
        let users = [
            { id: 1, name: '张三', email: 'zhangsan@example.com', role: 'admin' },
            { id: 2, name: '李四', email: 'lisi@example.com', role: 'user' },
            { id: 3, name: '王五', email: 'wangwu@example.com', role: 'user' }
        ];
        let nextId = 4;

        function logRequest(method, uri, status, data) {
            const logArea = document.getElementById('logArea');
            const cls = method.toLowerCase();
            logArea.innerHTML = `<span class="log-method log-${cls}">${method}</span> <span class="log-uri">${uri}</span>\n  → <span class="log-status">${status}</span> ${JSON.stringify(data).substring(0, 120)}\n\n` + logArea.innerHTML;
        }

        function listUsers() {
            logRequest('GET', '/api/v1/users', '200 OK', users);
            renderTable();
        }

        function renderTable() {
            document.getElementById('userTableBody').innerHTML = users.map(u => `
                <tr>
                    <td>${u.id}</td><td>${u.name}</td><td>${u.email}</td>
                    <td>
                        <button class="action-btn" style="background:#1a73e8" onclick="showEdit(${u.id})">PUT</button>
                        <button class="action-btn" style="background:#9c27b0" onclick="deleteUser(${u.id})">DELETE</button>
                    </td>
                </tr>
            `).join('');
        }

        function showCreate() {
            document.getElementById('formArea').innerHTML = `
                <h3 style="color:#ea4335;margin-bottom:12px;">POST /api/v1/users</h3>
                <div class="form-group"><label>姓名</label><input id="f-name" placeholder="姓名"></div>
                <div class="form-group"><label>邮箱</label><input id="f-email" placeholder="邮箱"></div>
                <div class="form-group"><label>角色</label><input id="f-role" value="user"></div>
                <button class="crud-btn btn-post" style="width:100%;margin-top:8px;" onclick="createUser()">POST 创建</button>
            `;
        }

        function createUser() {
            const name = document.getElementById('f-name').value;
            const email = document.getElementById('f-email').value;
            const role = document.getElementById('f-role').value || 'user';
            if (!name || !email) return alert('请填写姓名和邮箱');
            const user = { id: nextId++, name, email, role };
            users.push(user);
            logRequest('POST', '/api/v1/users', '201 Created', user);
            renderTable();
        }

        function showEdit(id) {
            const user = users.find(u => u.id === id);
            if (!user) return;
            document.getElementById('formArea').innerHTML = `
                <h3 style="color:#1a73e8;margin-bottom:12px;">PUT /api/v1/users/${id}</h3>
                <div class="form-group"><label>姓名</label><input id="f-name" value="${user.name}"></div>
                <div class="form-group"><label>邮箱</label><input id="f-email" value="${user.email}"></div>
                <div class="form-group"><label>角色</label><input id="f-role" value="${user.role}"></div>
                <button class="crud-btn btn-put" style="width:100%;margin-top:8px;" onclick="updateUser(${id})">PUT 更新</button>
            `;
        }

        function updateUser(id) {
            const name = document.getElementById('f-name').value;
            const email = document.getElementById('f-email').value;
            const role = document.getElementById('f-role').value;
            const index = users.findIndex(u => u.id === id);
            if (index === -1) return;
            users[index] = { id, name, email, role };
            logRequest('PUT', `/api/v1/users/${id}`, '200 OK', users[index]);
            renderTable();
        }

        function deleteUser(id) {
            const deleted = users.find(u => u.id === id);
            users = users.filter(u => u.id !== id);
            logRequest('DELETE', `/api/v1/users/${id}`, '204 No Content', deleted);
            renderTable();
        }
    </script>
</body>
</html>

示例3:API设计规范检查器

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>API设计规范检查器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        textarea { width: 100%; padding: 14px; border: 1px solid #dadce0; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.6; resize: vertical; min-height: 150px; }
        textarea:focus { outline: none; border-color: #1a73e8; }
        button { padding: 12px 28px; background: #1a73e8; color: #fff; border: none; border-radius: 8px; font-weight: 700; cursor: pointer; font-size: 15px; margin-top: 12px; }
        button:hover { background: #1557b0; }
        .check-list { list-style: none; }
        .check-item { padding: 12px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: flex-start; gap: 10px; }
        .check-icon { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; margin-top: 2px; }
        .icon-pass { background: #e8f5e9; color: #2e7d32; }
        .icon-fail { background: #ffebee; color: #c62828; }
        .icon-warn { background: #fff3e0; color: #ef6c00; }
        .check-info { flex: 1; }
        .check-name { font-weight: 600; font-size: 14px; }
        .check-detail { font-size: 13px; color: #666; margin-top: 2px; }
        .score-bar { height: 8px; background: #e8eaed; border-radius: 4px; overflow: hidden; margin: 16px 0; }
        .score-fill { height: 100%; border-radius: 4px; transition: width 0.5s; }
    </style>
</head>
<body>
    <div class="container">
        <h1>API设计规范检查器</h1>
        <div class="card">
            <h2>输入API端点列表</h2>
            <textarea id="apiInput" placeholder="每行一个API端点,格式:METHOD URI
例如:
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/1
PUT /api/v1/users/1
DELETE /api/v1/users/1">GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/1
PUT /api/v1/users/1
DELETE /api/v1/users/1
GET /api/v1/users?role=admin
POST /api/v1/users/1/activation</textarea>
            <button onclick="checkAPI()">检查规范</button>
        </div>
        <div class="card" id="resultCard" style="display:none;">
            <h2>检查结果</h2>
            <div class="score-bar"><div class="score-fill" id="scoreFill"></div></div>
            <ul class="check-list" id="checkList"></ul>
        </div>
    </div>
    <script>
        function checkAPI() {
            const input = document.getElementById('apiInput').value.trim();
            if (!input) return;
            const lines = input.split('\n').filter(l => l.trim());
            const endpoints = lines.map(l => {
                const parts = l.trim().split(/\s+/);
                return { method: parts[0], uri: parts.slice(1).join(' ') };
            });

            const checks = [];

            // 检查1:URI使用名词
            const verbPatterns = /\/(get|create|update|delete|list|find|search|add|remove|edit)\b/i;
            endpoints.forEach(ep => {
                if (verbPatterns.test(ep.uri)) {
                    checks.push({ pass: false, name: 'URI使用动词', detail: `${ep.method} ${ep.uri} - URI应使用名词而非动词,操作语义由HTTP方法表达` });
                }
            });
            if (!endpoints.some(ep => verbPatterns.test(ep.uri))) {
                checks.push({ pass: true, name: 'URI使用名词', detail: '所有URI使用名词表示资源,操作由HTTP方法表达' });
            }

            // 检查2:URI使用复数
            const singularPattern = /\/user(?!s)|\/order(?!s)|\/product(?!s)|\/article(?!s)/;
            endpoints.forEach(ep => {
                if (singularPattern.test(ep.uri) && !ep.uri.match(/\/\d/)) {
                    checks.push({ pass: false, name: 'URI使用单数', detail: `${ep.method} ${ep.uri} - 集合资源应使用复数形式(如/users而非/user)` });
                }
            });
            if (!endpoints.some(ep => singularPattern.test(ep.uri) && !ep.uri.match(/\/\d/))) {
                checks.push({ pass: true, name: 'URI使用复数', detail: '集合资源URI使用复数形式' });
            }

            // 检查3:使用小写和连字符
            const upperPattern = /[A-Z]|_/;
            endpoints.forEach(ep => {
                if (upperPattern.test(ep.uri)) {
                    checks.push({ pass: false, name: 'URI格式不规范', detail: `${ep.method} ${ep.uri} - URI应使用小写字母和连字符` });
                }
            });
            if (!endpoints.some(ep => upperPattern.test(ep.uri))) {
                checks.push({ pass: true, name: 'URI格式规范', detail: 'URI使用小写字母和连字符' });
            }

            // 检查4:版本号
            const hasVersion = endpoints.some(ep => /\/v\d+/.test(ep.uri));
            checks.push({ pass: hasVersion, name: 'API版本管理', detail: hasVersion ? 'URI中包含版本号' : '建议在URI中添加版本号(如/api/v1/users)' });

            // 检查5:HTTP方法与操作匹配
            const methods = new Set(endpoints.map(ep => ep.method));
            checks.push({ pass: methods.has('GET'), name: 'GET方法', detail: methods.has('GET') ? '使用GET获取资源' : '缺少GET方法获取资源' });
            checks.push({ pass: methods.has('POST'), name: 'POST方法', detail: methods.has('POST') ? '使用POST创建资源' : '缺少POST方法创建资源' });

            // 检查6:嵌套层级
            endpoints.forEach(ep => {
                const depth = (ep.uri.match(/\//g) || []).length;
                if (depth > 4) {
                    checks.push({ pass: false, name: 'URI嵌套过深', detail: `${ep.method} ${ep.uri} - 嵌套层级不超过3层` });
                }
            });

            // 检查7:查询参数用于过滤
            const hasQuery = endpoints.some(ep => ep.uri.includes('?'));
            checks.push({ pass: true, name: '查询参数', detail: hasQuery ? '使用查询参数进行过滤和分页' : '建议使用查询参数进行过滤、排序和分页', warn: !hasQuery });

            // 渲染结果
            const passCount = checks.filter(c => c.pass).length;
            const score = Math.round((passCount / checks.length) * 100);
            document.getElementById('scoreFill').style.width = score + '%';
            document.getElementById('scoreFill').style.background = score >= 80 ? '#34a853' : score >= 60 ? '#ff9800' : '#ea4335';

            document.getElementById('checkList').innerHTML = checks.map(c => `
                <li class="check-item">
                    <div class="check-icon ${c.pass ? 'icon-pass' : c.warn ? 'icon-warn' : 'icon-fail'}">${c.pass ? '' : c.warn ? '!' : ''}</div>
                    <div class="check-info">
                        <div class="check-name">${c.name}</div>
                        <div class="check-detail">${c.detail}</div>
                    </div>
                </li>
            `).join('');

            document.getElementById('resultCard').style.display = 'block';
        }
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 说明
Fetch API 42+ 39+ 10.1+ 14+ RESTful客户端
URLSearchParams 49+ 44+ 10.1+ 17+ 查询参数
JSON 全版本 全版本 全版本 全版本 数据格式
async/await 55+ 52+ 10.1+ 15+ 异步请求
AbortController 66+ 57+ 12.1+ 16+ 取消请求

注意事项与最佳实践

1. URI设计

  • 使用名词复数表示资源集合

  • 使用小写字母和连字符

  • 避免在URI中使用动词

  • 嵌套层级不超过3层

  • 使用查询参数进行过滤和分页

2. HTTP方法

  • 严格遵循方法语义:GET获取、POST创建、PUT替换、PATCH更新、DELETE删除

  • GET和DELETE不应有请求体

  • PUT需要提供完整资源数据

  • PATCH只提供修改的字段

3. 状态码

  • 使用最具体的状态码

  • 201表示创建成功,204表示删除成功

  • 400表示语法错误,422表示验证错误

  • 401和403不要混淆

4. 版本管理

  • 推荐URL路径版本化(/api/v1/)

  • 版本变更时保持向后兼容

  • 提供版本迁移指南

5. 错误处理

  • 返回统一的错误格式

  • 包含错误码、错误消息和详细信息

  • 不要暴露服务器内部信息

代码规范示例

RESTful API客户端

代码示例

class RestClient {
    constructor(baseURL, version = 'v1') {
        this.baseURL = `${baseURL}/api/${version}`;
    }

    async request(method, path, options = {}) {
        const url = `${this.baseURL}${path}`;
        const config = {
            method,
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                ...options.headers
            }
        };

        if (options.params) {
            const query = new URLSearchParams(options.params);
            url += `?${query}`;
        }

        if (options.body) {
            config.body = JSON.stringify(options.body);
        }

        const response = await fetch(url, config);

        if (!response.ok) {
            const error = await response.json().catch(() => ({}));
            throw new ApiError(response.status, error);
        }

        if (response.status === 204) return null;
        const result = await response.json();
        return result.data !== undefined ? result.data : result;
    }

    list(resource, params) { return this.request('GET', `/${resource}`, { params }); }
    get(resource, id) { return this.request('GET', `/${resource}/${id}`); }
    create(resource, data) { return this.request('POST', `/${resource}`, { body: data }); }
    update(resource, id, data) { return this.request('PUT', `/${resource}/${id}`, { body: data }); }
    patch(resource, id, data) { return this.request('PATCH', `/${resource}/${id}`, { body: data }); }
    delete(resource, id) { return this.request('DELETE', `/${resource}/${id}`); }
}

// 使用
const api = new RestClient('https://example.com');
const users = await api.list('users', { page: 1, size: 20 });
const user = await api.get('users', 1);
await api.create('users', { name: '张三' });
await api.update('users', 1, { name: '张三', email: 'new@example.com' });
await api.delete('users', 1);

常见问题与解决方案

问题1:非CRUD操作如何设计

解决方案:将操作视为子资源:

代码示例

POST /users/1/activation      // 激活
POST /orders/101/cancellation  // 取消
POST /articles/1/publish       // 发布

问题2:批量操作如何设计

解决方案

代码示例

// 批量删除
DELETE /users
Body: { "ids": [1, 2, 3] }

// 批量更新
PATCH /users
Body: { "updates": [{"id": 1, "role": "admin"}, {"id": 2, "role": "user"}] }

问题3:关联资源的URI设计

解决方案

代码示例

// 一对多关系
GET /users/1/orders          // 用户的订单
GET /orders/101/items        // 订单的商品

// 避免深层嵌套
// 不好:/users/1/orders/101/items/5
// 好:/order-items/5?order_id=101&user_id=1

问题4:API响应格式不统一

解决方案:定义统一的响应格式规范:

代码示例

{
    "code": 0,
    "message": "success",
    "data": {},
    "pagination": {},
    "links": {}
}

总结

RESTful API是现代Web应用的标准接口设计方式:

  1. REST原则:客户端-服务器、无状态、可缓存、统一接口、分层系统

  2. URI设计:名词复数、小写连字符、层级表示关系、查询参数过滤

  3. HTTP方法:GET获取、POST创建、PUT替换、PATCH更新、DELETE删除

  4. 状态码:200成功、201创建、204无内容、400参数错误、401未认证、403无权限、404未找到

  5. 分页排序:page/size/sort/order查询参数,响应包含分页元数据

  6. 版本管理:URL路径版本化(/api/v1/)最常用

  7. HATEOAS:响应包含相关操作链接(高级特性)

  8. 最佳实践:统一响应格式、规范错误处理、合理设计非CRUD操作

掌握RESTful API的设计原则和使用方法,能够让你更好地设计、实现和消费Web API。

常见问题

非CRUD操作如何设计

请参考教程中的详细说明和代码示例。

批量操作如何设计

请参考教程中的详细说明和代码示例。

什么是RESTful API?

RESTful API是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习RESTful API的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

RESTful API有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

RESTful API适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

RESTful API的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: Promise HTTP头部 RESTful API 表单提交 响应码 async await 键盘输入 数据展示

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 后端通信:async/await - 从入门到实践详解 下一篇: 后端通信:GraphQL基础 - 从入门到实践详解

poll相关推荐