Initial commit with translated description
This commit is contained in:
509
SKILL.md
Normal file
509
SKILL.md
Normal file
@@ -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`
|
||||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn7f6g2r31qsb1ts8cf5x7rpk180fn9j",
|
||||||
|
"slug": "api-dev",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publishedAt": 1770151676718
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user