| name | api-route-creator |
| description | Creates Next.js 16 API routes with auth, validation, and tenant scoping. Use when creating API endpoints. |
| allowed-tools | Read, Write, Edit, Glob |
| context | fork |
API Route Creation Skill
When to Use
Use this skill when creating:
- New API endpoints
- Route handlers
- Server actions
Security Requirements (NEVER VIOLATE)
- Always authenticate - Check session
- Always scope by tenant - Use session.user.tenantId
- Always validate input - Use Zod schemas
- Never trust user input - Especially tenant_id
- Log sensitive ops - Audit trail
Template: API Route Handler
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth/config';
import { z } from 'zod';
import { db } from '@/lib/db';
import { eq, and } from 'drizzle-orm';
import { tableName } from '@/lib/db/schema';
import { auditLogger } from '@/lib/audit/logger';
const inputSchema = z.object({
name: z.string().min(1).max(100).trim(),
description: z.string().max(500).optional(),
});
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
if (!['teacher', 'tenant_admin'].includes(session.user.role)) {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
const data = await db.query.tableName.findMany({
where: eq(tableName.tenantId, session.user.tenantId),
});
return NextResponse.json({ data });
} catch (error) {
console.error('[API] Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const body = await request.json();
const validatedInput = inputSchema.parse(body);
const [created] = await db.insert(tableName).values({
...validatedInput,
tenantId: session.user.tenantId,
createdBy: session.user.id,
}).returning();
await auditLogger.log({
action: 'CREATE',
resourceType: 'RESOURCE_NAME',
resourceId: created.id,
userId: session.user.id,
tenantId: session.user.tenantId,
});
return NextResponse.json({ data: created }, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation error', details: error.errors },
{ status: 400 }
);
}
console.error('[API] Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Template: Dynamic Route ([id])
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth/config';
import { db } from '@/lib/db';
import { eq, and } from 'drizzle-orm';
import { tableName } from '@/lib/db/schema';
interface RouteContext {
params: Promise<{ id: string }>;
}
export async function GET(
request: NextRequest,
context: RouteContext
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await context.params;
const item = await db.query.tableName.findFirst({
where: and(
eq(tableName.id, id),
eq(tableName.tenantId, session.user.tenantId)
),
});
if (!item) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json({ data: item });
} catch (error) {
console.error('[API] Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Error Response Format
{ error: 'Unauthorized' }
{ error: 'Forbidden' }
{ error: 'Not found' }
{ error: 'Validation error', details: [...] }
{ error: 'Internal server error' }
{ error: `Item ${id} not found in tenant ${tenantId}` }
Role Hierarchy
| Role | Can Access |
|---|
| student | Own data, joined assistants |
| teacher | Own assistants, class students |
| tenant_admin | All tenant data, user management |
| platform_admin | Everything (cross-tenant) |
Checklist