rest-api-design
23
总安装量
18
周安装量
#16476
全站排名
安装命令
npx skills add https://github.com/erichowens/some_claude_skills --skill rest-api-design
Agent 安装分布
claude-code
13
cursor
13
gemini-cli
12
antigravity
12
codex
11
Skill 文档
REST API Design
This skill helps you design and implement REST API endpoints following project patterns with Zod validation and OpenAPI documentation.
When to Use
â USE this skill for:
- Creating new REST API endpoints with Next.js App Router
- Designing request/response schemas with Zod
- Implementing proper error handling and status codes
- Adding rate limiting and authentication
- Generating OpenAPI documentation
â DO NOT use for:
- GraphQL APIs â different paradigm entirely
- Cloudflare Workers â use
cloudflare-worker-devskill - Supabase Edge Functions â use Supabase docs
- WebSocket/real-time APIs â different patterns
API Route Structure
src/app/api/
âââ auth/ # Authentication endpoints
âââ check-in/ # Daily check-in CRUD
âââ chat/ # AI coaching chat
âââ journal/ # Journal entries
âââ admin/ # Admin-only endpoints
âââ health/ # Health check
Standard Route Template
// src/app/api/[feature]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { getSession } from '@/lib/auth';
import { createRateLimiter } from '@/lib/rate-limit';
import { logPHIAccess } from '@/lib/hipaa/audit';
import { db } from '@/db';
// 1. Define schemas
const RequestSchema = z.object({
field: z.string().min(1).max(1000),
optional: z.string().optional(),
enumField: z.enum(['option1', 'option2']),
number: z.number().int().positive(),
});
const ResponseSchema = z.object({
id: z.string(),
createdAt: z.string().datetime(),
});
// 2. Configure rate limiter
const rateLimiter = createRateLimiter({
windowMs: 60000, // 1 minute
maxRequests: 30, // 30 requests per window
keyPrefix: 'api:feature',
});
// 3. Implement handlers
export async function GET(request: NextRequest) {
// Auth check
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Rate limit
const rateLimitResult = await rateLimiter.check(session.userId);
if (!rateLimitResult.allowed) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429, headers: rateLimitResult.headers }
);
}
// Query data
const data = await db.query.features.findMany({
where: eq(features.userId, session.userId),
});
// Audit log (if PHI)
await logPHIAccess(session.userId, 'feature', null, 'LIST');
return NextResponse.json(data);
}
export async function POST(request: NextRequest) {
// Auth check
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Rate limit
const rateLimitResult = await rateLimiter.check(session.userId);
if (!rateLimitResult.allowed) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429, headers: rateLimitResult.headers }
);
}
// Parse and validate body
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ error: 'Invalid JSON' },
{ status: 400 }
);
}
const parsed = RequestSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{
error: 'Validation failed',
details: parsed.error.issues.map(i => ({
path: i.path.join('.'),
message: i.message,
})),
},
{ status: 400 }
);
}
// Create resource
const [created] = await db.insert(features).values({
id: generateId(),
userId: session.userId,
...parsed.data,
createdAt: new Date(),
}).returning();
// Audit log
await logPHIAccess(session.userId, 'feature', created.id, 'CREATE');
return NextResponse.json(created, { status: 201 });
}
Zod Schema Patterns
Basic Types
import { z } from 'zod';
const Schema = z.object({
// Strings
name: z.string().min(1).max(100),
email: z.string().email(),
url: z.string().url(),
uuid: z.string().uuid(),
// Numbers
count: z.number().int().positive(),
rating: z.number().min(1).max(5),
price: z.number().nonnegative(),
// Booleans
isActive: z.boolean(),
// Dates
date: z.string().datetime(),
dateOnly: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
// Enums
status: z.enum(['pending', 'approved', 'denied']),
// Arrays
tags: z.array(z.string()).min(1).max(10),
// Optional fields
notes: z.string().optional(),
metadata: z.record(z.string()).optional(),
// Nullable
deletedAt: z.string().datetime().nullable(),
});
Advanced Patterns
// Discriminated unions
const EventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
z.object({ type: z.literal('keypress'), key: z.string() }),
]);
// Refinements
const PasswordSchema = z.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[a-z]/, 'Must contain lowercase')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
// Transform
const DateSchema = z.string()
.datetime()
.transform(str => new Date(str));
// Preprocess (coerce types)
const NumberFromString = z.preprocess(
val => typeof val === 'string' ? parseInt(val, 10) : val,
z.number()
);
Query Parameter Validation
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(['asc', 'desc']).default('desc'),
status: z.enum(['all', 'active', 'archived']).optional(),
});
const query = QuerySchema.safeParse({
page: searchParams.get('page'),
limit: searchParams.get('limit'),
sort: searchParams.get('sort'),
status: searchParams.get('status'),
});
if (!query.success) {
return NextResponse.json(
{ error: 'Invalid query parameters', details: query.error.issues },
{ status: 400 }
);
}
const { page, limit, sort, status } = query.data;
// Use validated params...
}
Error Response Format
// Standard error response
interface APIError {
error: string; // Human-readable message
code?: string; // Machine-readable code
details?: ErrorDetail[]; // Validation details
}
interface ErrorDetail {
path: string;
message: string;
}
// Error responses
return NextResponse.json(
{ error: 'Not found', code: 'NOT_FOUND' },
{ status: 404 }
);
return NextResponse.json(
{
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: [
{ path: 'email', message: 'Invalid email format' },
],
},
{ status: 400 }
);
HTTP Status Codes
| Code | Use Case |
|---|---|
| 200 | Successful GET, PUT, PATCH |
| 201 | Successful POST (created) |
| 204 | Successful DELETE (no content) |
| 400 | Invalid request/validation error |
| 401 | Not authenticated |
| 403 | Not authorized (authenticated but forbidden) |
| 404 | Resource not found |
| 409 | Conflict (duplicate, etc.) |
| 429 | Rate limit exceeded |
| 500 | Server error |
OpenAPI Documentation
Update docs/openapi.yaml when adding endpoints:
paths:
/api/feature:
get:
summary: List features
tags: [Features]
security:
- cookieAuth: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Feature'
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create feature
tags: [Features]
security:
- cookieAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateFeatureRequest'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/Feature'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
components:
schemas:
Feature:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
createdAt:
type: string
format: date-time
required: [id, name, createdAt]
CreateFeatureRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
required: [name]
responses:
Unauthorized:
description: Not authenticated
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: Unauthorized
ValidationError:
description: Validation failed
content:
application/json:
schema:
type: object
properties:
error:
type: string
details:
type: array
items:
type: object
properties:
path:
type: string
message:
type: string
Route Handler Patterns
Dynamic Routes
// src/app/api/feature/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
// Validate ID format
if (!isValidUUID(id)) {
return NextResponse.json(
{ error: 'Invalid ID format' },
{ status: 400 }
);
}
const item = await db.query.features.findFirst({
where: eq(features.id, id),
});
if (!item) {
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
return NextResponse.json(item);
}
Pagination
interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
async function getPaginated(page: number, limit: number) {
const offset = (page - 1) * limit;
const [data, [{ count }]] = await Promise.all([
db.query.features.findMany({
limit,
offset,
orderBy: desc(features.createdAt),
}),
db.select({ count: count() }).from(features),
]);
return {
data,
pagination: {
page,
limit,
total: count,
totalPages: Math.ceil(count / limit),
},
};
}