OA0 = Omni AI 0
OA0 是一个探索 AI 的论坛
现在注册
已注册用户请  登录
OA0  ›  技能包  ›  api-dev: REST 与 GraphQL API 的脚手架、测试、文档和调试工具

api-dev: REST 与 GraphQL API 的脚手架、测试、文档和调试工具

 
  app ·  2026-02-01 04:22:37 · 3 次点击  · 0 条评论  

name: api-dev
description: 脚手架、测试、文档化和调试 REST 与 GraphQL API。适用于用户需要创建 API 端点、编写集成测试、生成 OpenAPI 规范、使用 curl 测试、模拟 API 或排查 HTTP 问题。
metadata: {"clawdbot":{"emoji":"🔌","requires":{"anyBins":["curl","node","python3"]},"os":["linux","darwin","win32"]}}


API 开发

从命令行构建、测试、文档化和调试 HTTP API。涵盖完整的 API 生命周期:搭建端点、使用 curl 测试、生成 OpenAPI 文档、模拟服务以及调试。

适用场景

  • 搭建新的 REST 或 GraphQL 端点
  • 使用 curl 或脚本测试 API
  • 生成或验证 OpenAPI/Swagger 规范
  • 为开发模拟外部 API
  • 调试 HTTP 请求/响应问题
  • 对端点进行负载测试

使用 curl 测试 API

GET 请求

# 基础 GET 请求
curl -s https://api.example.com/users | jq .

# 携带请求头
curl -s -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" \
  https://api.example.com/users | jq .

# 携带查询参数
curl -s "https://api.example.com/users?page=2&limit=10" | jq .

# 同时显示响应头
curl -si https://api.example.com/users

POST/PUT/PATCH/DELETE 请求

# POST JSON 数据
curl -s -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"name": "Alice", "email": "alice@example.com"}' | jq .

# PUT 请求(完全替换)
curl -s -X PUT https://api.example.com/users/123 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Updated", "email": "alice@example.com"}' | jq .

# PATCH 请求(部分更新)
curl -s -X PATCH https://api.example.com/users/123 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice V2"}' | jq .

# DELETE 请求
curl -s -X DELETE https://api.example.com/users/123

# POST 表单数据
curl -s -X POST https://api.example.com/upload \
  -F "file=@document.pdf" \
  -F "description=My document"

调试请求

# 详细输出(查看完整请求/响应)
curl -v https://api.example.com/health 2>&1

# 仅显示响应头
curl -sI https://api.example.com/health

# 显示时间细分
curl -s -o /dev/null -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nFirst byte: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://api.example.com/health

# 跟随重定向
curl -sL https://api.example.com/old-endpoint

# 将响应保存到文件
curl -s -o response.json https://api.example.com/data

API 测试脚本

Bash 测试运行器

#!/bin/bash
# api-test.sh - 简单的 API 测试运行器
BASE_URL="${1:-http://localhost:3000}"
PASS=0
FAIL=0

assert_status() {
  local method="$1" url="$2" expected="$3" body="$4"
  local args=(-s -o /dev/null -w "%{http_code}" -X "$method")
  if [ -n "$body" ]; then
    args+=(-H "Content-Type: application/json" -d "$body")
  fi
  local status
  status=$(curl "${args[@]}" "$BASE_URL$url")
  if [ "$status" = "$expected" ]; then
    echo "PASS: $method $url -> $status"
    ((PASS++))
  else
    echo "FAIL: $method $url -> $status (expected $expected)"
    ((FAIL++))
  fi
}

assert_json() {
  local url="$1" jq_expr="$2" expected="$3"
  local actual
  actual=$(curl -s "$BASE_URL$url" | jq -r "$jq_expr")
  if [ "$actual" = "$expected" ]; then
    echo "PASS: GET $url | jq '$jq_expr' = $expected"
    ((PASS++))
  else
    echo "FAIL: GET $url | jq '$jq_expr' = $actual (expected $expected)"
    ((FAIL++))
  fi
}

# 健康检查
assert_status GET /health 200

# CRUD 测试
assert_status POST /api/users 201 '{"name":"Test","email":"test@test.com"}'
assert_status GET /api/users 200
assert_json /api/users '.[-1].name' 'Test'
assert_status DELETE /api/users/1 204

# 认证测试
assert_status GET /api/admin 401
assert_status GET /api/admin 403  # 使用错误的角色

echo ""
echo "结果: $PASS 通过, $FAIL 失败"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1

Python 测试运行器

#!/usr/bin/env python3
"""api_test.py - API 集成测试套件。"""
import json, sys, urllib.request, urllib.error

BASE = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:3000"
PASS = FAIL = 0

def request(method, path, body=None, headers=None):
    """发起 HTTP 请求,返回 (状态码, 响应体字典, 响应头)。"""
    url = f"{BASE}{path}"
    data = json.dumps(body).encode() if body else None
    hdrs = {"Content-Type": "application/json", "Accept": "application/json"}
    if headers:
        hdrs.update(headers)
    req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
    try:
        resp = urllib.request.urlopen(req)
        body = json.loads(resp.read().decode()) if resp.read() else None
    except urllib.error.HTTPError as e:
        return e.code, None, dict(e.headers)
    return resp.status, body, dict(resp.headers)

def test(name, fn):
    """运行测试函数,统计通过/失败。"""
    global PASS, FAIL
    try:
        fn()
        print(f"  PASS: {name}")
        PASS += 1
    except AssertionError as e:
        print(f"  FAIL: {name} - {e}")
        FAIL += 1

def assert_eq(actual, expected, msg=""):
    assert actual == expected, f"得到 {actual},预期 {expected}。{msg}"

# --- 测试用例 ---
print(f"正在测试 {BASE}\n")

test("GET /health 返回 200", lambda: (
    assert_eq(request("GET", "/health")[0], 200)
))

test("POST /api/users 创建用户", lambda: (
    assert_eq(request("POST", "/api/users", {"name": "Test", "email": "t@t.com"})[0], 201)
))

test("GET /api/users 返回数组", lambda: (
    assert_eq(type(request("GET", "/api/users")[1]), list)
))

test("GET /api/notfound 返回 404", lambda: (
    assert_eq(request("GET", "/api/notfound")[0], 404)
))

print(f"\n结果: {PASS} 通过, {FAIL} 失败")
sys.exit(0 if FAIL == 0 else 1)

OpenAPI 规范生成

从现有端点生成

# 根据 curl 响应搭建 OpenAPI 3.0 规范骨架
# 运行此命令,然后填充详细信息
cat > openapi.yaml << 'EOF'
openapi: "3.0.3"
info:
  title: 我的 API
  version: "1.0.0"
  description: API 描述信息
servers:
  - url: http://localhost:3000
    description: 本地开发环境
paths:
  /health:
    get:
      summary: 健康检查
      responses:
        "200":
          description: 服务运行正常
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
  /api/users:
    get:
      summary: 用户列表
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: 用户列表
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
    post:
      summary: 创建用户
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUser"
      responses:
        "201":
          description: 用户已创建
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: 验证错误
  /api/users/{id}:
    get:
      summary: 根据 ID 获取用户
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: 用户详情
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "404":
          description: 未找到
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
          format: email
        createdAt:
          type: string
          format: date-time
    CreateUser:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
        email:
          type: string
          format: email
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
EOF

验证 OpenAPI 规范

# 使用 npx(无需安装)
npx @redocly/cli lint openapi.yaml

# 快速检查:YAML 是否有效?
python3 -c "import yaml; yaml.safe_load(open('openapi.yaml'))" && echo "YAML 有效"

模拟服务器

使用 Python 快速模拟

#!/usr/bin/env python3
"""mock_server.py - 基于 OpenAPI 风格配置的轻量级 API 模拟。"""
import json, http.server, re, sys

PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080

# 定义模拟路由: (方法, 路径模式) -> 响应
ROUTES = {
    ("GET", "/health"): {"status": 200, "body": {"status": "ok"}},
    ("GET", "/api/users"): {"status": 200, "body": [
        {"id": "1", "name": "Alice", "email": "alice@example.com"},
        {"id": "2", "name": "Bob", "email": "bob@example.com"},
    ]},
    ("POST", "/api/users"): {"status": 201, "body": {"id": "3", "name": "Created"}},
    ("GET", r"/api/users/\w+"): {"status": 200, "body": {"id": "1", "name": "Alice"}},
    ("DELETE", r"/api/users/\w+"): {"status": 204, "body": None},
}

class MockHandler(http.server.BaseHTTPRequestHandler):
    def _handle(self):
        for (method, pattern), response in ROUTES.items():
            if self.command == method and re.fullmatch(pattern, self.path.split('?')[0]):
                self.send_response(response["status"])
                if response["body"] is not None:
                    self.send_header("Content-Type", "application/json")
                    self.end_headers()
                    self.wfile.write(json.dumps(response["body"]).encode())
                else:
                    self.end_headers()
                return
        self.send_response(404)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({"error": "Not found"}).encode())

    do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = _handle

    def log_message(self, fmt, *args):
        print(f"{self.command} {self.path} -> {args[1] if len(args) > 1 else '?'}")

print(f"模拟服务器运行于 http://localhost:{PORT}")
http.server.HTTPServer(("", PORT), MockHandler).serve_forever()

运行:python3 mock_server.py 8080

Node.js Express 脚手架

最小化 REST API

// server.js - 最小化 Express REST API
const express = require('express');
const app = express();
app.use(express.json());

// 内存存储
const items = new Map();
let nextId = 1;

// CRUD 端点
app.get('/api/items', (req, res) => {
  const { page = 1, limit = 20 } = req.query;
  const all = [...items.values()];
  const start = (page - 1) * limit;
  res.json({ items: all.slice(start, start + +limit), total: all.length });
});

app.get('/api/items/:id', (req, res) => {
  const item = items.get(req.params.id);
  if (!item) return res.status(404).json({ error: 'Not found' });
  res.json(item);
});

app.post('/api/items', (req, res) => {
  const { name, description } = req.body;
  if (!name) return res.status(400).json({ error: 'name required' });
  const id = String(nextId++);
  const item = { id, name, description: description || '', createdAt: new Date().toISOString() };
  items.set(id, item);
  res.status(201).json(item);
});

app.put('/api/items/:id', (req, res) => {
  if (!items.has(req.params.id)) return res.status(404).json({ error: 'Not found' });
  const item = { ...req.body, id: req.params.id, updatedAt: new Date().toISOString() };
  items.set(req.params.id, item);
  res.json(item);
});

app.delete('/api/items/:id', (req, res) => {
  if (!items.has(req.params.id)) return res.status(404).json({ error: 'Not found' });
  items.delete(req.params.id);
  res.status(204).end();
});

// 错误处理器
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API 运行于 http://localhost:${PORT}`));

设置

mkdir my-api && cd my-api
npm init -y
npm install express
node server.js

调试模式

检查端口是否被占用

# Linux/macOS
lsof -i :3000
# 或
ss -tlnp | grep 3000

# 终止占用端口的进程
kill $(lsof -t -i :3000)

测试 CORS

# 预检请求
curl -s -X OPTIONS https://api.example.com/users \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -I

监控响应时间退化

# 快速基准测试(10 个请求)
for i in $(seq 1 10); do
  curl -s -o /dev/null -w "%{time_total}\n" http://localhost:3000/api/users
done | awk '{sum+=$1; if($1>max)max=$1} END {printf "平均: %.3fs, 最大: %.3fs\n", sum/NR, max}'

检查 JWT 令牌

# 解码 JWT 载荷(不验证签名)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

实用技巧

  • 使用 jq 处理 JSON 响应:curl -s url | jq '.items[] | {id, name}'
  • 对于携带请求体的请求,务必设置 Content-Type 请求头,缺失可能导致静默的 400 错误
  • 使用 -w '\n' 确保 curl 输出以换行符结尾
  • 对于大型响应体,可管道至 jq -C . | less -R 进行彩色分页查看
  • 测试错误路径:无效 JSON、缺失字段、类型错误、未授权、未找到
  • WebSocket 测试:npx wscat -c ws://localhost:3000/ws
3 次点击  ∙  0 人收藏  
登录后收藏  
目前尚无回复
0 条回复
About   ·   Help   ·    
OA0 - Omni AI 0 一个探索 AI 的社区
沪ICP备2024103595号-2
Developed with Cursor