Claude-skill-registry api-handler
StepLeague API route pattern using withApiHandler wrapper. Use when creating or modifying any API route in the /api directory. Keywords: API, route, endpoint, handler, auth, POST, GET, PUT, DELETE, validation.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/api-handler" ~/.claude/skills/majiayu000-claude-skill-registry-api-handler && rm -rf "$T"
manifest:
skills/data/api-handler/SKILL.mdsource content
API Handler Skill
Overview
Use
withApiHandler for all API routes. It eliminates boilerplate and ensures consistent auth, validation, and error handling.
Basic Usage
import { withApiHandler } from "@/lib/api/handler"; import { z } from "zod"; const mySchema = z.object({ name: z.string(), count: z.number().optional(), }); export const POST = withApiHandler({ auth: 'required', schema: mySchema, }, async ({ user, body, adminClient }) => { const { data } = await adminClient .from("table") .insert({ ...body, user_id: user.id }) .select() .single(); return { success: true, data }; });
Auth Levels
| Level | Description | Context Provided |
|---|---|---|
| No auth required | may be null |
| Must be logged in | guaranteed |
| Site-wide superadmin | guaranteed, verified superadmin |
| Must be league member | , |
| Must be admin or owner | , (admin/owner role) |
| Must be owner | , (owner role) |
League Auth Examples
// Any league member can access export const GET = withApiHandler({ auth: 'league_member', }, async ({ user, membership }) => { // membership.role is 'owner', 'admin', or 'member' return { role: membership?.role }; }); // Only admins and owners export const PUT = withApiHandler({ auth: 'league_admin', schema: updateSchema, }, async ({ user, body, adminClient, membership }) => { // membership.role is guaranteed 'admin' or 'owner' return { updated: true }; });
League ID Resolution
For league auth, the handler looks for
league_id in this order:
- Request body (
){ league_id: "..." } - URL params (
)/api/leagues/[id] - Query params (
)?league_id=...
Handler Context
The handler function receives:
interface HandlerContext<T> { user: User | null; // Authenticated user body: T; // Parsed & validated request body adminClient: SupabaseClient; // Admin client (bypasses RLS) request: Request; // Original request params: Record<string, string>; // URL params (e.g., { id: 'xxx' }) membership: Membership | null; // For league_* auth levels }
Schema Validation
Use Zod for request validation:
const createSchema = z.object({ name: z.string().min(1).max(100), description: z.string().optional(), is_active: z.boolean().default(true), league_id: z.string().uuid(), }); export const POST = withApiHandler({ auth: 'required', schema: createSchema, }, async ({ body }) => { // body is fully typed and validated console.log(body.name); // string console.log(body.is_active); // boolean (defaulted to true if not provided) });
Validation Errors
If validation fails, the handler automatically returns:
{ "error": "Validation failed: name: Required, league_id: Invalid uuid" }
Returning Responses
Return Object (Auto-wrapped)
return { success: true, data }; // Becomes: Response with JSON { success: true, data }
Return Response Directly
import { json, badRequest, forbidden } from "@/lib/api"; // Custom status or headers return json({ data }, { status: 201 }); // Error responses return badRequest("Invalid input"); return forbidden("Not allowed");
Error Handling
Errors thrown in the handler are caught and logged:
export const POST = withApiHandler({ auth: 'required', }, async ({ adminClient }) => { const { data, error } = await adminClient.from("table").insert({}); if (error) { // Use AppError for typed errors throw new AppError({ code: ErrorCode.DB_INSERT_FAILED, message: error.message, context: { table: 'table' }, }); } return { success: true }; });
Reference the
error-handling skill for more on AppError.
Complete Example
// src/app/api/leagues/[id]/members/route.ts import { withApiHandler } from "@/lib/api/handler"; import { z } from "zod"; import { AppError, ErrorCode } from "@/lib/errors"; const addMemberSchema = z.object({ user_id: z.string().uuid(), role: z.enum(['member', 'admin']).default('member'), }); // GET - List members (any league member can view) export const GET = withApiHandler({ auth: 'league_member', }, async ({ params, adminClient }) => { const { data } = await adminClient .from("memberships") .select("*, users(*)") .eq("league_id", params.id); return { members: data }; }); // POST - Add member (only admins/owners) export const POST = withApiHandler({ auth: 'league_admin', schema: addMemberSchema, }, async ({ params, body, adminClient }) => { const { data, error } = await adminClient .from("memberships") .insert({ league_id: params.id, user_id: body.user_id, role: body.role, }) .select() .single(); if (error) { throw new AppError({ code: ErrorCode.DB_INSERT_FAILED, message: "Failed to add member", context: { error: error.message }, }); } return { success: true, member: data }; });
Legacy Pattern
For existing routes not yet migrated:
import { createServerSupabaseClient, createAdminClient } from "@/lib/supabase/server"; import { json, badRequest, unauthorized } from "@/lib/api"; export async function GET(request: Request) { const supabase = await createServerSupabaseClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return unauthorized(); const adminClient = createAdminClient(); const { data } = await adminClient.from("table").select("*"); return json({ data }); }
Rule: Use
withApiHandler for all NEW routes. Migrate legacy routes only when modifying them for other reasons.
Reference Files
| File | Purpose |
|---|---|
| The withApiHandler implementation |
| Response helpers (json, badRequest, etc.) |
Related Skills
- Database operations with adminClientsupabase-patterns
- Error codes and AppError usageerror-handling
- Why we use this patternarchitecture-philosophy