| name | api-handler |
| description | 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. |
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 |
|---|
'none' | No auth required | user may be null |
'required' | Must be logged in | user guaranteed |
'superadmin' | Site-wide superadmin | user guaranteed, verified superadmin |
'league_member' | Must be league member | user, membership |
'league_admin' | Must be admin or owner | user, membership (admin/owner role) |
'league_owner' | Must be owner | user, membership (owner role) |
League Auth Examples
export const GET = withApiHandler({
auth: 'league_member',
}, async ({ user, membership }) => {
return { role: membership?.role };
});
export const PUT = withApiHandler({
auth: 'league_admin',
schema: updateSchema,
}, async ({ user, body, adminClient, membership }) => {
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;
body: T;
adminClient: SupabaseClient;
request: Request;
params: Record<string, string>;
membership: Membership | null;
}
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 }) => {
console.log(body.name);
console.log(body.is_active);
});
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 };
Return Response Directly
import { json, badRequest, forbidden } from "@/lib/api";
return json({ data }, { status: 201 });
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) {
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
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'),
});
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 };
});
export const POST = withApiHandler({
auth: 'league_admin',
: addMemberSchema,
}, ({ params, body, adminClient }) => {
{ data, error } = adminClient
.()
.({
: params.,
: body.,
: body.,
})
.()
.();
(error) {
({
: .,
: ,
: { : error. },
});
}
{ : , : 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 |
|---|
src/lib/api/handler.ts | The withApiHandler implementation |
src/lib/api.ts | Response helpers (json, badRequest, etc.) |
Related Skills
supabase-patterns - Database operations with adminClient
error-handling - Error codes and AppError usage
architecture-philosophy - Why we use this pattern