Automated summary
Guides the creation and review of Next.js App Router API routes in a specific repository by enforcing the handleRoute / requireRole / parseJsonBody trio and cursor-based pagination.
Strengths
- Provides a directly copy-pasteable route.ts skeleton with the repository's exact imports and helpers
- Explains why handleRoute is the single error boundary (envelope mapping, Prisma P2025 to 404, trace_id) instead of just mandating it
- Gives the full cursor-pagination idiom: take limit+1, id tie-break, page.next_cursor
- Ties each rule to a concrete precedent (QA finding H-1, review finding HI-1), making the guidance verifiable rather than arbitrary
Limitations
- The 'Common Mistakes' section is cut off: the table is announced but empty, so the pitfall list is incomplete
- Tightly coupled to one repository (services/app/src/app/api paths, auth/http modules, api/openapi specs) and not portable as-is
- Assumes familiarity with parseQuery, ok/created/noContent and the AppError hierarchy, none of which are included
Use it when adding an HTTP method to an existing route or creating a new Next.js endpoint in this repository, including when reviewing a route.ts diff.
Avoid it for other projects or non-App-Router stacks (Pages Router, Express, tRPC) whose conventions and helpers differ.
Security analysis
SafeThe skill is pure documentation: it prescribes a coding convention for Next.js API routes (wrapping handlers in handleRoute, requireRole auth checks, zod validation, cursor pagination). It contains no shell commands, no network or file-system operations, no credential handling, and no instructions to bypass safety controls. Code samples are illustrative TypeScript calling internal helpers, and the only executable guidance is 'avoid hand-rolled try/catch', which is a quality convention rather than a risky action. No prompt-injection or role-manipulation attempt was present in the content.
No concerns found
How this analysis is madeExamples
Add a GET /api/products endpoint that lists products with an optional name filter and cursor pagination, following the API route scaffold conventions.Add a DELETE handler to services/app/src/app/api/stock/entries/route.ts that requires the stock:write role and returns 204.Review this route.ts diff for missing requireRole or handleRoute calls, raw req.json() parsing, and ad-hoc try/catch error handling.name: api-route-scaffold description: Use when adding a new Next.js App Router API route under services/app/src/app/api/** — new endpoint file (route.ts), new HTTP method on an existing route, or a new list/GET endpoint needing pagination. Also use when reviewing a route.ts diff for missing requireRole/handleRoute or ad-hoc error handling.
API Route Scaffold
Overview
All 46+ route files under services/app/src/app/api/**/route.ts follow one shape:
handleRoute(async () => { requireRole/requireSession; parseJsonBody/parseQuery; call the domain module; return ok/created/noContent }). handleRoute (in services/app/src/lib/http.ts)
is the ONLY place that translates errors to the API's standard {code, message, details, trace_id} envelope (api/openapi/_common.yaml) — domain code never formats an HTTP response
itself. Deviating from this (hand-rolled try/catch, skipping requireRole, validating with raw
await req.json()) reintroduces exactly the bug QA finding H-1 fixed: an uncaught Prisma P2025
("no record found") falling through to a generic 500 instead of the documented 404.
The Pattern (evidence: stock/entries/route.ts, products/route.ts)
import { z } from "zod";
import { requireRole } from "@/modules/auth";
import { doTheThing } from "@/modules/<domain>";
import { created, handleRoute, ok, parseJsonBody, parseQuery } from "@/lib/http";
const BodySchema = z.object({ /* zod schema mirroring api/openapi/<domain>.yaml */ });
export async function POST(req: Request): Promise<Response> {
return handleRoute(async () => {
const ctx = await requireRole("<domain>:write"); // throws 403 if role lacks permission
const input = await parseJsonBody(req, BodySchema); // throws 400 ValidationError on bad JSON/shape
const result = await doTheThing(ctx.tenantId, ctx.userId, input);
return created(result); // 201; use ok() for 200, noContent() for 204
});
}
Never write your own try { ... } catch { return Response.json(...) } — handleRoute already:
maps any AppError subclass (NotFoundError, ValidationError, ForbiddenError,
UnauthorizedError, ...) to its documented status/body; maps Prisma's P2025
(findUniqueOrThrow/findFirstOrThrow on an RLS-invisible or missing row) to NotFoundError
automatically — this is why domain code is free to use findUniqueOrThrow directly instead of
manual null-checks; logs genuinely unexpected errors server-side with a trace_id (never leaked
to the client) so failures are traceable instead of silent 500s.
List Endpoints: Cursor Pagination Convention
Every GET list endpoint (evidence: catalog/products.ts listProducts,
catalog/suppliers.ts) uses the same Prisma cursor idiom, never offset/page-number pagination:
// Route: query schema
const ListQuerySchema = z.object({
cursor: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).optional(), // ALWAYS clamp -- see HI-1 below
// ...domain filters
});
// Module: query
const limit = filters.limit ?? 20;
const rows = await tx.someModel.findMany({
where: { /* ... */ },
orderBy: [{ createdAt: "desc" }, { id: "desc" }], // tie-break on id -- required for a stable cursor
take: limit + 1, // fetch one extra to detect has_more
...(filters.cursor ? { cursor: { id: filters.cursor }, skip: 1 } : {}),
});
const hasMore = rows.length > limit;
const page = hasMore ? rows.slice(0, limit) : rows;
return { items: page, page: { next_cursor: hasMore ? (page.at(-1)?.id ?? null) : null, has_more: hasMore } };
If the list needs a derived value per row (e.g. current stock), batch it with ONE query over the
whole page's ids (see getCurrentStockForProducts in stock-operation-scaffold), never one
query per row in a loop — that exact per-row loop was code-reviewer finding HI-1 across four
endpoints, precisely because a hand-written list endpoint skipped this convention.
Common Mistakes
| Mistake | Fix |
|---------|-----|
| Custom try/catch instead of handleRoute | Always wrap the whole handler body in handleRoute(async () => { ... }) |
| Calling the domain function before requireRole/requireSession | Auth check is always the FIRST line inside handleRoute's callback |
| Validating body with raw await req.json() | Use parseJsonBody(req, ZodSchema) — gets the standard 400 ValidationError shape for free |
| Unbounded limit on a list endpoint | z.coerce.number().int().min(1).max(100).optional() — an unclamped limit turns a list endpoint into an unbounded query |
| Per-row query in a .map/loop over a list result | Batch with WHERE id = ANY($1)-style query over the whole page, per HI-1 |
| New AppError-like condition without a subclass | Add/reuse an AppError subclass in libs/shared so handleRoute/toErrorResponse map it automatically — don't format Response.json by hand in domain code |
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/next-js-api-route-scaffolding-mvda8739/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/next-js-api-route-scaffolding-mvda8739' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/next-js-api-route-scaffolding-mvda8739/download' --output '.skill-downloads/next-js-api-route-scaffolding-mvda8739/SKILL.md.part' && mv '.skill-downloads/next-js-api-route-scaffolding-mvda8739/SKILL.md.part' '.skill-downloads/next-js-api-route-scaffolding-mvda8739/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.
Claude Code
test -f '.skill-downloads/next-js-api-route-scaffolding-mvda8739/SKILL.md' && test -s '.skill-downloads/next-js-api-route-scaffolding-mvda8739/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/api-route-scaffold' && cp -R '.skill-downloads/next-js-api-route-scaffolding-mvda8739' '.claude/skills/api-route-scaffold'OpenSkills
OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.
Official OpenSkills installationopenskills install './.skill-downloads/next-js-api-route-scaffolding-mvda8739'openskills read 'next-js-api-route-scaffolding-mvda8739'Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.
For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.
openskills syncManual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/next-js-api-route-scaffolding-mvda8739/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.