Skip to content
api-patterns logo

API Patterns Skill

api-patterns

API route implementation patterns with RLS, Zod validation, and error handling. Use when creating API routes, implementing endpoints, or adding server-side validation.

SKILL.md

Full skill instructions

API Patterns Skill

Purpose

Route to existing API patterns and provide checklists for safe, validated API route implementation. All API routes MUST use RLS context helpers—see rls-patterns skill.

When This Skill Applies

Invoke this skill when:

  • Creating new API routes
  • Implementing CRUD endpoints
  • Adding request/​response validation
  • Handling webhooks
  • Implementing error handling patterns

Authoritative References (MUST READ)

PatternLocationPurpose
User Context APIdocs/​patterns/​api/​user-context-api.mdUser-scoped operations
Admin Context APIdocs/​patterns/​api/​admin-context-api.mdAdmin-scoped operations
Zod Validationdocs/​patterns/​api/​zod-validation-api.mdRequest/​response validation
Webhook Handlerdocs/​patterns/​api/​webhook-handler.mdWebhook processing
Bonus Contentdocs/​patterns/​api/​bonus-content-delivery.mdProtected content delivery

Stop-the-Line Conditions

FORBIDDEN Patterns

// FORBIDDEN: Direct Prisma calls (bypass RLS)
const users = await prisma.user.findMany();
// Must use: withUserContext, withAdminContext, or withSystemContext

// FORBIDDEN: Missing authentication check
export async function GET(req: Request) {
  return getUserData(); // No auth check!
}

// FORBIDDEN: Unvalidated user input
const { userId } = await req.json();
// Must validate with Zod schema

// FORBIDDEN: Generic error responses
return new Response("Error", { status: 500 });
// Must use structured error response

CORRECT Patterns

// CORRECT: RLS context + auth check
export async function GET(req: Request) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const data = await withUserContext(prisma, userId, async (client) => {
    return client.user.findUnique({ where: { user_id: userId } });
  });

  return NextResponse.json(data);
}

// CORRECT: Zod validation
const schema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

const result = schema.safeParse(body);
if (!result.success) {
  return NextResponse.json(
    { error: "Validation failed", details: result.error.flatten() },
    { status: 400 },
  );
}

API Route Checklist

Before ANY API route:

  • Authentication check with await auth() from Clerk
  • Proper 401 response for unauthenticated
  • Request validation with Zod schema
  • RLS context wrapper (withUserContext/withAdminContext/withSystemContext)
  • Structured error responses with appropriate status codes
  • TypeScript types for request/​response

Standard Response Patterns

Success Response

return NextResponse.json({ data, success: true }, { status: 200 });

Error Response

return NextResponse.json(
  {
    error: "Human-readable error message",
    code: "ERROR_CODE",
    details: optional_details,
  },
  { status: 400 | 401 | 403 | 404 | 500 },
);

Status Codes

CodeWhen to Use
200Success
201Created (POST)
400Bad request / validation error
401Not authenticated
403Forbidden (authenticated but not authorized)
404Resource not found
500Server error

API Route Template

import { auth } from "@clerk/​nextjs/​server";
import { NextResponse } from "next/​server";
import { z } from "zod";
import { withUserContext } from "@/​lib/​rls-helpers";
import { prisma } from "@/​lib/​prisma";

// Request validation schema
const RequestSchema = z.object({
  // Define expected fields
});

export async function POST(req: Request) {
  try {
    // 1. Authenticate
    const { userId } = await auth();
    if (!userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // 2. Parse and validate request
    const body = await req.json();
    const result = RequestSchema.safeParse(body);
    if (!result.success) {
      return NextResponse.json(
        { error: "Validation failed", details: result.error.flatten() },
        { status: 400 },
      );
    }

    // 3. Execute with RLS context
    const data = await withUserContext(prisma, userId, async (client) => {
      return client.resource.create({ data: result.data });
    });

    // 4. Return success response
    return NextResponse.json({ data, success: true }, { status: 201 });
  } catch (error) {
    console.error("API error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 },
    );
  }
}

API Documentation Template

For documenting new endpoints:

## Endpoint: POST /​api/​resource

### Description

Creates a new resource for the authenticated user.

### Authentication

Required: Clerk session

### Request Body

| Field | Type   | Required | Description   |
| ----- | ------ | -------- | ------------- |
| name  | string | Yes      | Resource name |
| type  | string | No       | Resource type |

### Response

**Success (201)**:
\`\`\`json
{ "data": { "id": 1, "name": "..." }, "success": true }
\`\`\`

**Error (400)**:
\`\`\`json
{ "error": "Validation failed", "details": {...} }
\`\`\`

### RLS Context

Uses `withUserContext` - user can only access own resources.

Related Skills

  • rls-patterns: RLS context helper usage (REQUIRED for all DB operations)
  • security-audit: API security validation
  • testing-patterns: API endpoint testing