Instruction file imported from dima-vlkdmn/ai-task-agent (
.cursor/rules/api-design.mdc). Copyright stays with the author.
API Design
Route Handler Pattern
export async function METHOD(request: NextRequest, ctx?: RouteContext<"...">) {
// 1. Rate limit check (AI/expensive endpoints only)
// 2. Parse body: request.json().catch(() => null)
// 3. Validate: Schema.safeParse(body) → validationErrorResponse on failure
// 4. Call application layer in try/catch → toErrorResponse in catch
// 5. Return Response.json(result, { status: 200 | 201 })
}
Error Response Shape
Every error response — 400, 404, 429, 500 — uses this exact shape:
{ "error": { "code": "STRING_CODE", "message": "Human readable message" } }
Validation errors add details:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid input", "details": {...} } }
Status Codes
| Status | Meaning | When to use |
|---|---|---|
| 200 | OK | GET, PATCH success |
| 201 | Created | POST that creates a resource |
| 400 | Bad Request | Failed Zod validation |
| 404 | Not Found | Resource not found (check before updating) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Error | Unexpected infrastructure failure |
Error Utilities (from src/lib/http.ts, src/lib/errors.ts)
toErrorResponse(err) // use in every catch block
validationErrorResponse(zodError) // use when safeParse fails
Errors.notFound("Task not found") // use for 404 — returns null from repo
Errors.rateLimited(retryAfterSeconds) // use in rate limit guard
Naming Conventions
GET /api/tasks → list all
POST /api/tasks/from-text → create from AI (action route)
PATCH /api/tasks/[id] → update one field
No DELETE endpoint in MVP scope.