mit einem Klick
creating-api-routes
// Scaffold new Next.js API route handlers with auth guards, Zod validation, error handling, and integration tests. Use when creating new API endpoints, adding CRUD operations, or building route handlers.
// Scaffold new Next.js API route handlers with auth guards, Zod validation, error handling, and integration tests. Use when creating new API endpoints, adding CRUD operations, or building route handlers.
| name | creating-api-routes |
| description | Scaffold new Next.js API route handlers with auth guards, Zod validation, error handling, and integration tests. Use when creating new API endpoints, adding CRUD operations, or building route handlers. |
Scaffolds complete API route handlers following the project's request lifecycle pattern, including validation schemas and integration tests.
Use the checklist below and track your progress:
Progress:
- [ ] Step 1: Create the validation schema
- [ ] Step 2: Create the route handler
- [ ] Step 3: Create the integration test
- [ ] Step 4: Verify the route works
Step 1: Create the validation schema
Create a Zod schema in src/lib/validations/{entity}.ts:
import { z } from "zod";
import { nameFieldSchema } from "@/lib/validations/shared";
export const createTeamSchema = z.object({
name: nameFieldSchema,
});
export type CreateTeamInput = z.infer<typeof createTeamSchema>;
src/lib/validations/shared.ts for reusable field schemas before defining new onescreate{Entity}Schema, update{Entity}Schemaexport type {Action}{Entity}Input = z.infer<typeof schema>Step 2: Create the route handler
Create the route file at src/app/api/{resource}/route.ts (or src/app/api/{resource}/[id]/route.ts for single-resource routes).
Every handler follows this exact skeleton:
import { NextResponse } from "next/server";
import { requireAuth, isAuthFailure } from "@/lib/api-auth";
import { validateBody, isValidationError, handleRouteError } from "@/lib/api-helpers";
import { getDb } from "@/lib/db";
import { EntityNameEntity } from "@/entities/entity-name.entity";
import { createEntitySchema } from "@/lib/validations/entity-name";
export async function GET() {
const auth = await requireAuth();
if (isAuthFailure(auth)) return auth;
try {
const dataSource = await getDb();
const repo = dataSource.getRepository(EntityNameEntity);
const items = await repo.find();
return NextResponse.json(items);
} catch (error) {
return handleRouteError(error, "GET /api/resource");
}
}
export async function POST(request: Request) {
const auth = await requireAuth();
if (isAuthFailure(auth)) return auth;
const parsed = await validateBody(request, createEntitySchema);
if (isValidationError(parsed)) return parsed;
try {
const dataSource = await getDb();
const repo = dataSource.getRepository(EntityNameEntity);
const entity = repo.create(parsed.data);
const saved = await repo.save(entity);
return NextResponse.json(saved, { status: 201 });
} catch (error) {
return handleRouteError(error, "POST /api/resource", {
uniqueViolationMessage: "Resource with this name already exists",
});
}
}
For routes with URL params ([id]):
type RouteContext = { params: Promise<{ id: string }> };
export async function GET(request: Request, context: RouteContext) {
const auth = await requireAuth();
if (isAuthFailure(auth)) return auth;
const { id } = await context.params;
const entityId = parseEntityId(id);
if (entityId === null) return invalidIdResponse("resource");
try {
const dataSource = await getDb();
const repo = dataSource.getRepository(EntityNameEntity);
const item = await repo.findOneBy({ id: entityId });
if (!item) throw new NotFoundError("Resource not found");
return NextResponse.json(item);
} catch (error) {
return handleRouteError(error, "GET /api/resource/:id");
}
}
Step 3: Create the integration test
Create the test file at src/app/api/{resource}/__tests__/route.test.ts:
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from "vitest";
import { DataSource } from "typeorm";
import { getTestDataSource, destroyTestDataSource, cleanDatabase } from "@/test/db-helpers";
import { EntityNameEntity } from "@/entities/entity-name.entity";
let testDs: DataSource;
let mockCookieStore: Record<string, string> = {};
vi.mock("next/headers", () => ({
cookies: async () => ({
get: (name: string) => {
const value = mockCookieStore[name];
return value !== undefined ? { value } : undefined;
},
}),
}));
vi.mock("@/lib/db", () => ({ getDb: async () => testDs }));
const { GET, POST } = await import("@/app/api/resource/route");
beforeAll(async () => { testDs = await getTestDataSource(); });
afterAll(async () => { await destroyTestDataSource(); });
beforeEach(async () => { await cleanDatabase(testDs); mockCookieStore = {}; });
async function seedAuthSession(): Promise<void> {
const { hashPassword, createSession, SESSION_COOKIE_NAME } = await import("@/lib/auth");
const { UserEntity } = await import("@/entities/user.entity");
const userRepo = testDs.getRepository(UserEntity);
const user = await userRepo.save({
username: "testadmin",
passwordHash: await hashPassword("testpass"),
});
const token = await createSession(user.id);
mockCookieStore[SESSION_COOKIE_NAME] = token;
}
describe("GET /api/resource", () => {
it("returns 401 without auth", async () => {
const response = await GET();
expect(response.status).toBe(401);
});
it("returns items", async () => {
await seedAuthSession();
// seed test data...
const response = await GET();
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toHaveLength(1);
});
});
describe("POST /api/resource", () => {
it("creates a new item", async () => {
await seedAuthSession();
const request = new Request("http://localhost/api/resource", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Test" }),
});
const response = await POST(request);
expect(response.status).toBe(201);
});
});
Key rules:
vi.mock() before await import() for correct mock resolutionStep 4: Verify the route works
npx vitest run src/app/api/{resource}/__tests__/route.test.tsnpx tsc --noEmitnpm run lintcreating-entities — create the entity before creating the API routewriting-integration-tests — detailed test patterns beyond the basics covered herewriting-e2e-tests — add E2E coverage for the new endpoint's UIScaffold background jobs with node-cron scheduling, pessimistic locking via acquireJobLock, and instrumentation.ts registration. Use when creating scheduled jobs, adding cron tasks, or implementing background processing.
Scaffold TypeORM EntitySchema definitions with TypeScript interfaces, database migrations, and Zod validation schemas. Use when creating new database entities, adding tables, or modifying existing entity structures.
Write Playwright E2E tests with direct DB seeding, API-based login, accessible selectors, and dual-config awareness. Use when creating end-to-end tests, testing user flows, or verifying UI behavior.
Write Vitest integration tests against a real PostgreSQL test database with proper mock setup, seed functions, and assertion patterns. Use when creating tests for API routes, lib modules, or database operations.