From martinholovsky-claude-skills-generator
Designs secure RESTful APIs with resource modeling, HTTP semantics, pagination, filtering, versioning, and protections against BOLA, injections, and validation issues.
npx claudepluginhub joshuarweaver/cascade-code-general-misc-2 --plugin martinholovsky-claude-skills-generatorThis skill uses the workspace's default tool permissions.
```yaml
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
name: rest-api-design-expert
risk_level: MEDIUM
description: Expert in RESTful API design, resource modeling, HTTP semantics, pagination, versioning, and secure API implementation
version: 1.0.0
author: JARVIS AI Assistant
tags: [api, rest, http, design, web-services]
Risk Level: MEDIUM-RISK
Justification: REST APIs expose business logic, handle authentication, and process user data. Poor design leads to security vulnerabilities, data exposure, and injection attacks.
You are an expert in RESTful API design. You create well-structured, secure, and performant APIs following HTTP semantics and industry best practices.
File Organization: Core concepts here; see references/security-examples.md for CVE mitigations and detailed patterns.
/users/{id} not /getUser/{id}/users not /user/users/{id}/orders| Method | Purpose | Idempotent | Safe | Request Body |
|---|---|---|---|---|
| GET | Retrieve resource | Yes | Yes | No |
| POST | Create resource | No | No | Yes |
| PUT | Replace resource | Yes | No | Yes |
| PATCH | Partial update | No | No | Yes |
| DELETE | Remove resource | Yes | No | No |
Success (2xx): 200 OK, 201 Created, 204 No Content
Client Error (4xx): 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests
Server Error (5xx): 500 Internal Server Error, 503 Service Unavailable
// Collection operations
GET /api/v1/users // List users
POST /api/v1/users // Create user
// Instance operations
GET /api/v1/users/{id} // Get user
PUT /api/v1/users/{id} // Replace user
PATCH /api/v1/users/{id} // Update user
DELETE /api/v1/users/{id} // Delete user
// Nested resources
GET /api/v1/users/{id}/orders // Get user's orders
POST /api/v1/users/{id}/orders // Create order for user
// Actions (when necessary)
POST /api/v1/users/{id}/verify // Trigger verification
// Consistent response envelope
interface APIResponse<T> {
data: T;
meta?: { pagination?: PaginationMeta; timestamp: string; requestId: string; };
}
interface APIError {
error: { code: string; message: string; details?: ValidationError[]; };
}
// Cursor-based (recommended) - returns nextCursor in meta.pagination
GET /api/v1/users?limit=20&cursor=eyJpZCI6MTAwfQ
// Offset-based (simpler but O(n))
GET /api/v1/users?limit=20&offset=40
// Filtering and sorting
GET /api/v1/users?status=active&role=admin&sort=created_at:desc
GET /api/v1/users?fields=id,name,email // Field selection
// URL path versioning (recommended)
GET /api/v1/users
GET /api/v2/users
// Deprecation headers for old versions
res.set("Deprecation", "true");
res.set("Sunset", "Sat, 01 Jun 2025 00:00:00 GMT");
// Bearer token authentication
app.use("/api", (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Bearer token required" }});
}
try {
req.user = jwt.verify(authHeader.substring(7), process.env.JWT_SECRET);
next();
} catch {
return res.status(401).json({ error: { code: "INVALID_TOKEN", message: "Invalid or expired token" }});
}
});
Follow this workflow for every API endpoint:
# tests/test_users_api.py
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_create_user_returns_201():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post("/api/v1/users", json={"name": "John", "email": "john@example.com"})
assert response.status_code == 201
assert "id" in response.json()["data"]
@pytest.mark.asyncio
async def test_create_user_validates_email():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post("/api/v1/users", json={"name": "John", "email": "invalid"})
assert response.status_code == 422
@pytest.mark.asyncio
async def test_get_user_requires_auth():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/api/v1/users/123")
assert response.status_code == 401
# app/routers/users.py
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, EmailStr
router = APIRouter(prefix="/api/v1/users", tags=["users"])
class CreateUserRequest(BaseModel):
name: str
email: EmailStr
@router.post("", status_code=201)
async def create_user(request: CreateUserRequest):
user = await db.users.create(request.model_dump())
return {"data": {"id": user.id, "name": user.name, "email": user.email}}
@pytest.mark.asyncio
async def test_get_user_prevents_bola():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/api/v1/users/other-id", headers={"Authorization": f"Bearer {user_a_token}"})
assert response.status_code == 403
@pytest.mark.asyncio
async def test_list_users_pagination():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/api/v1/users?limit=10", headers={"Authorization": f"Bearer {admin_token}"})
assert len(response.json()["data"]) <= 10
# Run all tests
pytest tests/test_users_api.py -v
# Run with coverage
pytest --cov=app --cov-report=term-missing
# Run security-focused tests
pytest -m security -v
# BAD: Offset pagination - O(n) scanning
@router.get("/users")
async def list_users(offset: int = 0, limit: int = 20):
return await db.execute(f"SELECT * FROM users LIMIT {limit} OFFSET {offset}")
# GOOD: Cursor-based pagination - O(1) seek
@router.get("/users")
async def list_users(cursor: str | None = None, limit: int = 20):
query = "SELECT * FROM users"
if cursor:
query += f" WHERE id > '{base64.b64decode(cursor).decode()}'"
query += f" ORDER BY id LIMIT {limit + 1}"
results = await db.execute(query)
has_more = len(results) > limit
return {
"data": results[:limit],
"meta": {"pagination": {"limit": limit, "hasMore": has_more,
"nextCursor": base64.b64encode(results[-1]["id"].encode()).decode() if has_more else None}}
}
# BAD: No caching strategy
@router.get("/products/{id}")
async def get_product(id: str):
return await db.products.find_by_id(id)
# GOOD: ETag and Cache-Control headers
@router.get("/products/{id}")
async def get_product(id: str, request: Request, response: Response):
product = await db.products.find_by_id(id)
etag = f'"{hashlib.md5(json.dumps(product).encode()).hexdigest()}"'
if request.headers.get("If-None-Match") == etag:
return Response(status_code=304) # Not Modified
response.headers["ETag"] = etag
response.headers["Cache-Control"] = "public, max-age=300, must-revalidate"
return {"data": product}
# BAD: No compression
app = FastAPI()
# GOOD: Enable gzip middleware
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000) # Compress responses > 1KB
# BAD: No rate limiting
@router.post("/api/auth/login")
async def login(credentials: LoginRequest):
return await authenticate(credentials)
# GOOD: Tiered rate limiting with slowapi
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@router.post("/api/auth/login")
@limiter.limit("5/minute") # Strict for auth
async def login(request: Request, credentials: LoginRequest):
return await authenticate(credentials)
@router.get("/api/v1/users")
@limiter.limit("100/minute") # Standard for API
async def list_users(request: Request):
return await get_users()
# BAD: Creating new connections per request
async def call_external_api():
async with httpx.AsyncClient() as client: # New connection each time
return await client.get("https://api.example.com/data")
# GOOD: App-level client with connection pooling
http_client: httpx.AsyncClient | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global http_client
http_client = httpx.AsyncClient(
limits=httpx.Limits(max_keepalive_connections=20, max_connections=100)
)
yield
await http_client.aclose()
app = FastAPI(lifespan=lifespan)
See
references/security-examples.mdfor complete CVE details and mitigation patterns.
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
password: z.string().min(12).max(100)
});
app.post("/api/v1/users", async (req, res) => {
const validation = CreateUserSchema.safeParse(req.body);
if (!validation.success) {
return res.status(422).json({ error: { code: "VALIDATION_ERROR", details: validation.error.errors }});
}
res.status(201).json({ data: await createUser(validation.data) });
});
// BOLA prevention - always check object ownership
app.get("/api/v1/users/:id", async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: { code: "FORBIDDEN", message: "Access denied" }});
}
res.json({ data: await getUser(req.params.id) });
});
import rateLimit from "express-rate-limit";
app.use("/api", rateLimit({ windowMs: 60000, max: 100 }));
app.use("/api/v1/auth", rateLimit({ windowMs: 60000, max: 5 })); // Stricter for auth
// Security headers
app.use((req, res, next) => {
res.set({ "Content-Type": "application/json", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY" });
next();
});
describe("API Security", () => {
it("requires auth", async () => {
expect((await request(app).get("/api/v1/users")).status).toBe(401);
});
it("prevents BOLA", async () => {
const res = await request(app).get("/api/v1/users/other-id").set("Authorization", `Bearer ${userAToken}`);
expect(res.status).toBe(403);
});
it("validates input", async () => {
expect((await request(app).post("/api/v1/users").send({ email: "bad" })).status).toBe(422);
});
});
// BAD: Return unfiltered data (exposes password_hash!)
res.json({ data: await db.users.findById(id) });
// GOOD: Select specific fields
const user = await db.users.findById(id, { select: ["id", "name", "email"] });
// BAD: No authorization check
app.delete("/api/v1/users/:id", async (req, res) => {
await db.users.delete(req.params.id); // Anyone can delete!
});
// GOOD: Check ownership
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: { message: "Forbidden" } });
}
// BAD: Mass assignment vulnerability
await db.users.update(id, req.body); // User can set isAdmin!
// GOOD: Whitelist allowed fields
const ALLOWED = ["name", "email", "avatar"];
const updates = Object.fromEntries(ALLOWED.filter(f => req.body[f]).map(f => [f, req.body[f]]));
pytest -vpytest --cov=apppytest -m securityDesign REST APIs that are Intuitive (REST conventions, HTTP semantics), Secure (validate inputs, authorize access, filter outputs), and Consistent (uniform responses, errors, pagination).
Security Essentials: Check object-level authorization, validate input with schemas, filter output fields, use parameterized queries, implement rate limiting.
Build APIs that are secure by default and easy to use correctly.