From ff3b7e3dd98852d6574eaed474b711c2fa8eb459 Mon Sep 17 00:00:00 2001 From: zlei9 Date: Sun, 29 Mar 2026 09:43:14 +0800 Subject: [PATCH] Initial commit with translated description --- SKILL.md | 509 +++++++++++++++++++++++++++++++++++++++++++++++++++++ _meta.json | 6 + 2 files changed, 515 insertions(+) create mode 100644 SKILL.md create mode 100644 _meta.json diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..5813b63 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,509 @@ +--- +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 Development + +Build, test, document, and debug HTTP APIs from the command line. Covers the full API lifecycle: scaffolding endpoints, testing with curl, generating OpenAPI docs, mocking services, and debugging. + +## When to Use + +- Scaffolding new REST or GraphQL endpoints +- Testing APIs with curl or scripts +- Generating or validating OpenAPI/Swagger specs +- Mocking external APIs for development +- Debugging HTTP request/response issues +- Load testing endpoints + +## Testing APIs with curl + +### GET requests + +```bash +# Basic GET +curl -s https://api.example.com/users | jq . + +# With headers +curl -s -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json" \ + https://api.example.com/users | jq . + +# With query params +curl -s "https://api.example.com/users?page=2&limit=10" | jq . + +# Show response headers too +curl -si https://api.example.com/users +``` + +### POST/PUT/PATCH/DELETE + +```bash +# 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 (full replace) +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 (partial update) +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 form data +curl -s -X POST https://api.example.com/upload \ + -F "file=@document.pdf" \ + -F "description=My document" +``` + +### Debug requests + +```bash +# Verbose output (see full request/response) +curl -v https://api.example.com/health 2>&1 + +# Show only response headers +curl -sI https://api.example.com/health + +# Show timing breakdown +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 + +# Follow redirects +curl -sL https://api.example.com/old-endpoint + +# Save response to file +curl -s -o response.json https://api.example.com/data +``` + +## API Test Scripts + +### Bash test runner + +```bash +#!/bin/bash +# api-test.sh - Simple API test runner +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 +} + +# Health check +assert_status GET /health 200 + +# CRUD tests +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 + +# Auth tests +assert_status GET /api/admin 401 +assert_status GET /api/admin 403 # with wrong role + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] && exit 0 || exit 1 +``` + +### Python test runner + +```python +#!/usr/bin/env python3 +"""api_test.py - API integration test suite.""" +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): + """Make an HTTP request, return (status, body_dict, headers).""" + 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): + """Run a test function, track pass/fail.""" + 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"got {actual}, expected {expected}. {msg}" + +# --- Tests --- +print(f"Testing {BASE}\n") + +test("GET /health returns 200", lambda: ( + assert_eq(request("GET", "/health")[0], 200) +)) + +test("POST /api/users creates user", lambda: ( + assert_eq(request("POST", "/api/users", {"name": "Test", "email": "t@t.com"})[0], 201) +)) + +test("GET /api/users returns array", lambda: ( + assert_eq(type(request("GET", "/api/users")[1]), list) +)) + +test("GET /api/notfound returns 404", lambda: ( + assert_eq(request("GET", "/api/notfound")[0], 404) +)) + +print(f"\nResults: {PASS} passed, {FAIL} failed") +sys.exit(0 if FAIL == 0 else 1) +``` + +## OpenAPI Spec Generation + +### Generate from existing endpoints + +```bash +# Scaffold an OpenAPI 3.0 spec from curl responses +# Run this, then fill in the details +cat > openapi.yaml << 'EOF' +openapi: "3.0.3" +info: + title: My API + version: "1.0.0" + description: API description here +servers: + - url: http://localhost:3000 + description: Local development +paths: + /health: + get: + summary: Health check + responses: + "200": + description: Service is healthy + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ok + /api/users: + get: + summary: List users + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + - name: limit + in: query + schema: + type: integer + default: 20 + responses: + "200": + description: List of users + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + post: + summary: Create user + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUser" + responses: + "201": + description: User created + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "400": + description: Validation error + /api/users/{id}: + get: + summary: Get user by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: User details + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "404": + description: Not found +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 +``` + +### Validate OpenAPI spec + +```bash +# Using npx (no install needed) +npx @redocly/cli lint openapi.yaml + +# Quick check: is the YAML valid? +python3 -c "import yaml; yaml.safe_load(open('openapi.yaml'))" && echo "Valid YAML" +``` + +## Mock Server + +### Quick mock with Python + +```python +#!/usr/bin/env python3 +"""mock_server.py - Lightweight API mock from OpenAPI-like config.""" +import json, http.server, re, sys + +PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + +# Define mock routes: (method, path_pattern) -> response +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"Mock server on http://localhost:{PORT}") +http.server.HTTPServer(("", PORT), MockHandler).serve_forever() +``` + +Run: `python3 mock_server.py 8080` + +## Node.js Express Scaffolding + +### Minimal REST API + +```javascript +// server.js - Minimal Express REST API +const express = require('express'); +const app = express(); +app.use(express.json()); + +// In-memory store +const items = new Map(); +let nextId = 1; + +// CRUD endpoints +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(); +}); + +// Error handler +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 running on http://localhost:${PORT}`)); +``` + +### Setup + +```bash +mkdir my-api && cd my-api +npm init -y +npm install express +node server.js +``` + +## Debugging Patterns + +### Check if port is in use + +```bash +# Linux/macOS +lsof -i :3000 +# or +ss -tlnp | grep 3000 + +# Kill process on port +kill $(lsof -t -i :3000) +``` + +### Test CORS + +```bash +# Preflight request +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 +``` + +### Watch for response time regressions + +```bash +# Quick benchmark (10 requests) +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 "Avg: %.3fs, Max: %.3fs\n", sum/NR, max}' +``` + +### Inspect JWT tokens + +```bash +# Decode JWT payload (no verification) +echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq . +``` + +## Tips + +- Use `jq` for JSON response processing: `curl -s url | jq '.items[] | {id, name}'` +- Set `Content-Type` header on every request with a body - missing it causes silent 400s +- Use `-w '\n'` with curl to ensure output ends with a newline +- For large response bodies, pipe to `jq -C . | less -R` for colored paging +- Test error paths: invalid JSON, missing fields, wrong types, unauthorized, not found +- For WebSocket testing: `npx wscat -c ws://localhost:3000/ws` diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..38a3aab --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7f6g2r31qsb1ts8cf5x7rpk180fn9j", + "slug": "api-dev", + "version": "1.0.0", + "publishedAt": 1770151676718 +} \ No newline at end of file