From tour-dev-toolkit
This skill should be used when the user asks to "scaffold a route", "create an API route", "generate route handler", "new API endpoint", or needs a new API route handler following project conventions.
npx claudepluginhub boburshoh122000/claude-plugins-bobur --plugin tour-dev-toolkitThis skill uses the workspace's default tool permissions.
Generate an API route handler file at `src/app/api/{path}/route.ts` following project conventions.
Verifies tests pass on completed feature branch, presents options to merge locally, create GitHub PR, keep as-is or discard; executes choice and cleans up worktree.
Guides root cause investigation for bugs, test failures, unexpected behavior, performance issues, and build failures before proposing fixes.
Writes implementation plans from specs for multi-step tasks, mapping files and breaking into TDD bite-sized steps before coding.
Generate an API route handler file at src/app/api/{path}/route.ts following project conventions.
Accept a route path relative to src/app/api/ (e.g., tours/[id]/payments, cron/cleanup, organization/settings).
Determine which template to use based on the path:
cron/[param] segmentsUse for paths like organization/settings, tasks/bulk, v1/leads.
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auth } from '@/auth';
import { prisma } from '@/lib/db';
// ─── Zod Schemas ────────────────────────────────────────────────────────────
const CreateSchema = z.object({
// TODO: Define request body fields — use .gt(0) not .positive() (Zod v4)
name: z.string().min(1),
});
// ─── GET ────────────────────────────────────────────────────────────────────
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id || !session.user.organization_id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// TODO: Implement query — scope to organization_id
const records = await prisma.MODELNAME.findMany({
where: { organization_id: session.user.organization_id },
});
return NextResponse.json({ data: records });
} catch (error) {
console.error('[GET /api/{path}]', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ─── POST ───────────────────────────────────────────────────────────────────
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id || !session.user.organization_id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const validated = CreateSchema.parse(body);
// TODO: Implement creation — scope to organization_id
const record = await prisma.MODELNAME.create({
data: {
...validated,
organization_id: session.user.organization_id,
},
});
return NextResponse.json({ data: record }, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Validation failed', details: error.issues }, { status: 400 });
}
console.error('[POST /api/{path}]', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
Use for paths starting with cron/ (e.g., cron/cleanup, cron/daily-digest).
Security: cron routes authenticate via CRON_SECRET bearer token, not user sessions. Use verifyCronRequest() from @/lib/cron-guard.
import { NextRequest, NextResponse } from 'next/server';
import { verifyCronRequest, acquireCronLock, releaseCronLock } from '@/lib/cron-guard';
import { prisma } from '@/lib/db';
const LOCK_NAME = '{cron-job-name}';
/**
* Cron handler: {description}
*
* GET /api/cron/{cron-path}
*
* Security: CRON_SECRET header required (via verifyCronRequest).
*/
export async function GET(request: NextRequest) {
// 1. Auth — verify CRON_SECRET (timing-safe)
const guard = await verifyCronRequest(request);
if (guard) return guard;
// 2. Distributed lock — prevent overlapping runs
const acquired = await acquireCronLock(LOCK_NAME);
if (!acquired) {
return NextResponse.json({ skipped: true, reason: 'already running' });
}
const start = Date.now();
try {
// TODO: Implement cron logic
let processed = 0;
const errors: { id: string; error: string }[] = [];
// Example: iterate over items with per-item error isolation
// const items = await prisma.MODELNAME.findMany({ where: { ... }, take: 100 });
// for (const item of items) {
// try {
// // process item
// processed++;
// } catch (err) {
// const msg = err instanceof Error ? err.message : String(err);
// errors.push({ id: item.id, error: msg });
// console.error('[cron/{cron-path}] item failed', { id: item.id, error: msg });
// }
// }
const elapsed = Date.now() - start;
console.info('[cron/{cron-path}] done', { processed, errored: errors.length, elapsed });
return NextResponse.json({
timestamp: new Date().toISOString(),
processed,
errors,
elapsed,
});
} catch (error) {
console.error('[cron/{cron-path}] fatal', error);
return NextResponse.json({ error: 'Cron job failed' }, { status: 500 });
} finally {
await releaseCronLock(LOCK_NAME);
}
}
Use for paths containing [param] segments (e.g., tours/[id]/payments, travelers/[id]/documents).
Important: In Next.js 16, params is a Promise and must be awaited.
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auth } from '@/auth';
import { prisma } from '@/lib/db';
// ─── Zod Schemas ────────────────────────────────────────────────────────────
const UpdateSchema = z.object({
// TODO: Define request body fields — use .gt(0) not .positive() (Zod v4)
name: z.string().min(1),
});
// ─── GET ────────────────────────────────────────────────────────────────────
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const session = await auth();
if (!session?.user?.id || !session.user.organization_id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
// TODO: Implement query — scope to organization_id
const record = await prisma.MODELNAME.findFirst({
where: { id, organization_id: session.user.organization_id },
});
if (!record) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json({ data: record });
} catch (error) {
console.error('[GET /api/{path}]', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ─── PATCH ──────────────────────────────────────────────────────────────────
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const session = await auth();
if (!session?.user?.id || !session.user.organization_id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const body = await request.json();
const validated = UpdateSchema.parse(body);
// Org-scoped update via updateMany
const result = await prisma.MODELNAME.updateMany({
where: { id, organization_id: session.user.organization_id },
data: validated,
});
if (result.count === 0) {
return NextResponse.json({ error: 'Not found or access denied' }, { status: 404 });
}
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Validation failed', details: error.issues }, { status: 400 });
}
console.error('[PATCH /api/{path}]', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
// ─── DELETE ─────────────────────────────────────────────────────────────────
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const session = await auth();
if (!session?.user?.id || !session.user.organization_id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
// Verify record belongs to org before deletion
const record = await prisma.MODELNAME.findFirst({
where: { id, organization_id: session.user.organization_id },
});
if (!record) {
return NextResponse.json({ error: 'Not found or access denied' }, { status: 404 });
}
// Soft-delete (preferred) or hard-delete
await prisma.MODELNAME.update({
where: { id },
data: { deleted_at: new Date() },
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('[DELETE /api/{path}]', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
MODELNAME with the actual camelCase Prisma model name.{path}, {cron-path}, {cron-job-name}, and {description} placeholders with actual values.[id] and [docId]), update the params type to Promise<{ id: string; docId: string }>..gt(0) not .positive() (Zod v4).organization_id for multi-tenant isolation.