Skip to content
api-route-creator logo

API Route Creation Skill

api-route-creator

Creates Next.js 16 API routes with auth, validation, and tenant scoping. Use when creating API endpoints.

SKILL.md

Full skill instructions

API Route Creation Skill

When to Use

Use this skill when creating:

  • New API endpoints
  • Route handlers
  • Server actions

Security Requirements (NEVER VIOLATE)

  1. Always authenticate - Check session
  2. Always scope by tenant - Use session.user.tenantId
  3. Always validate input - Use Zod schemas
  4. Never trust user input - Especially tenant_id
  5. 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';

// Input validation schema
const inputSchema = z.object({
  name: z.string().min(1).max(100).trim(),
  description: z.string().max(500).optional(),
});

// GET - Read (with tenant scoping)
export async function GET(request: NextRequest) {
  try {
    const session = await auth();

    // Authentication check
    if (!session?.user) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    // Role check (if needed)
    if (!['teacher', 'tenant_admin'].includes(session.user.role)) {
      return NextResponse.json(
        { error: 'Forbidden' },
        { status: 403 }
      );
    }

    // ✅ CRITICAL: Always scope by tenant
    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 }
    );
  }
}

// POST - Create
export async function POST(request: NextRequest) {
  try {
    const session = await auth();

    if (!session?.user) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    // Parse and validate input
    const body = await request.json();
    const validatedInput = inputSchema.parse(body);

    // ✅ CRITICAL: Use session tenant, NEVER request body
    const [created] = await db.insert(tableName).values({
      ...validatedInput,
      tenantId: session.user.tenantId, // FROM SESSION ONLY
      createdBy: session.user.id,
    }).returning();

    // Audit log sensitive operations
    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 });
    }

    // ✅ Await params in Next.js 16
    const { id } = await context.params;

    // ✅ CRITICAL: Scope by BOTH id AND tenant
    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

// Standard error responses (FERPA-safe - no data leakage)
{ error: 'Unauthorized' }           // 401 - Not authenticated
{ error: 'Forbidden' }              // 403 - Wrong role
{ error: 'Not found' }              // 404 - Doesn't exist or wrong tenant
{ error: 'Validation error', details: [...] }  // 400 - Bad input
{ error: 'Internal server error' }  // 500 - Something broke

// ❌ NEVER expose internal details
{ error: `Item ${id} not found in tenant ${tenantId}` }  // WRONG!

Role Hierarchy

RoleCan Access
studentOwn data, joined assistants
teacherOwn assistants, class students
tenant_adminAll tenant data, user management
platform_adminEverything (cross-tenant)

Checklist

  • Session authentication
  • Role-based authorization
  • Tenant scoping on ALL queries
  • Input validation with Zod
  • Params awaited (Next.js 16)
  • FERPA-safe error messages
  • Audit logging for sensitive ops
  • TypeScript types complete