---
name: api-development
description: Create production-grade API endpoints following Mawidi's patterns including authentication, validation, rate limiting, CSRF protection, error handling, and tests. Use when adding new API routes, webhooks, integrations, or backend features. Generates scaffolded code with best practices.
---

# API Development Skill

## Overview

Create production-grade API endpoints following Mawidi's established patterns. This skill automates scaffolding while ensuring consistency with authentication, validation, rate limiting, CSRF protection, error handling, and test generation.

**Token Savings:** ~15,000 tokens per endpoint (scaffolding + validation + tests)

## When to Use This Skill

Use this skill when:

- Creating new API routes (`app/api/*`)
- Adding webhook endpoints (`api/webhooks/*`)
- Building new backend features
- Implementing external integrations
- Adding authenticated endpoints
- Creating public API endpoints

## Mawidi API Patterns

### Route File Location

```
mawidi-site/
└── app/
    └── api/
        ├── auth/              # Authentication routes
        │   └── [...nextauth]/ # NextAuth handler
        ├── bookings/          # Booking management
        │   ├── route.ts       # GET/POST bookings
        │   └── [id]/
        │       └── route.ts   # GET/PUT/DELETE single booking
        ├── stripe/            # Payment integration
        │   ├── checkout/
        │   └── webhook/
        └── users/             # User management
```

### Standard Route Template

```typescript
// app/api/[resource]/route.ts

import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { rateLimiter } from "@/lib/rate-limiter";
import { prisma } from "@/lib/db";
import { z } from "zod";

// Input validation schema
const createResourceSchema = z.object({
  name: z.string().min(1).max(255),
  email: z.string().email(),
  // Add fields as needed
});

/**
 * GET /api/resource
 * List all resources for authenticated user
 */
export async function GET(request: NextRequest) {
  try {
    // 1. Authentication
    const session = await getServerSession(authOptions);
    if (!session?.user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // 2. Rate limiting
    const rateLimitResult = await rateLimiter.check(
      request,
      "resource-list",
      100,
      "1m",
    );
    if (!rateLimitResult.success) {
      return NextResponse.json(
        { error: "Too many requests", retryAfter: rateLimitResult.retryAfter },
        { status: 429 },
      );
    }

    // 3. Fetch data
    const resources = await prisma.resource.findMany({
      where: { userId: session.user.id },
      orderBy: { createdAt: "desc" },
    });

    // 4. Return response
    return NextResponse.json({ data: resources });
  } catch (error: any) {
    console.error("GET /api/resource error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 },
    );
  }
}

/**
 * POST /api/resource
 * Create a new resource
 */
export async function POST(request: NextRequest) {
  try {
    // 1. Authentication
    const session = await getServerSession(authOptions);
    if (!session?.user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // 2. Rate limiting
    const rateLimitResult = await rateLimiter.check(
      request,
      "resource-create",
      10,
      "15m",
    );
    if (!rateLimitResult.success) {
      return NextResponse.json({ error: "Too many requests" }, { status: 429 });
    }

    // 3. Parse and validate input
    const body = await request.json();
    const validation = createResourceSchema.safeParse(body);

    if (!validation.success) {
      return NextResponse.json(
        { error: "Validation failed", details: validation.error.flatten() },
        { status: 400 },
      );
    }

    // 4. Create resource
    const resource = await prisma.resource.create({
      data: {
        ...validation.data,
        userId: session.user.id,
      },
    });

    // 5. Return response
    return NextResponse.json(
      { data: resource, message: "Resource created successfully" },
      { status: 201 },
    );
  } catch (error: any) {
    console.error("POST /api/resource error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 },
    );
  }
}
```

## Authentication Patterns

### Authenticated Routes (Most Common)

```typescript
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

export async function GET(request: NextRequest) {
  const session = await getServerSession(authOptions);

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

  // User available at session.user
  const userId = session.user.id;
  // ...
}
```

### Staff/Admin Routes

```typescript
import { validateStaffSession } from "@/lib/staff-session";

export async function GET(request: NextRequest) {
  const staffSession = await validateStaffSession(request);

  if (!staffSession) {
    return NextResponse.json(
      { error: "Staff access required" },
      { status: 403 },
    );
  }

  // Staff member validated
  // ...
}
```

### Public Routes (No Auth)

```typescript
// For public endpoints like health checks, public data
export async function GET(request: NextRequest) {
  // No authentication required
  const data = await prisma.publicResource.findMany();
  return NextResponse.json({ data });
}
```

### API Key Authentication

```typescript
// For external integrations
export async function POST(request: NextRequest) {
  const apiKey = request.headers.get("x-api-key");

  if (!apiKey || !isValidApiKey(apiKey)) {
    return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
  }

  // Proceed with request
}
```

## Validation Patterns

### Zod Schema Validation

```typescript
import { z } from "zod";

// Define schema
const bookingSchema = z.object({
  serviceId: z.string().uuid(),
  date: z.string().datetime(),
  notes: z.string().max(500).optional(),
  patientPhone: z.string().regex(/^\+?[1-9]\d{1,14}$/),
});

// Type inference
type BookingInput = z.infer<typeof bookingSchema>;

// Usage in route
const validation = bookingSchema.safeParse(body);
if (!validation.success) {
  return NextResponse.json(
    {
      error: "Validation failed",
      details: validation.error.flatten().fieldErrors,
    },
    { status: 400 },
  );
}
```

### Common Validation Patterns

```typescript
// Email
z.string().email();

// Phone (E.164)
z.string().regex(/^\+?[1-9]\d{1,14}$/);

// UUID
z.string().uuid();

// Optional with default
z.string().default("draft");

// Enum
z.enum(["pending", "confirmed", "cancelled"]);

// Date string
z.string().datetime();

// Array with limits
z.array(z.string()).min(1).max(10);

// Object with nested validation
z.object({
  address: z.object({
    street: z.string(),
    city: z.string(),
  }),
});
```

## Rate Limiting Patterns

### Standard Rate Limits

```typescript
import { rateLimiter } from "@/lib/rate-limiter";

// Endpoint-specific limits
const RATE_LIMITS = {
  "auth-login": { requests: 5, window: "15m" },
  "auth-register": { requests: 3, window: "1h" },
  "booking-create": { requests: 10, window: "15m" },
  "api-general": { requests: 100, window: "1m" },
  webhook: { requests: 1000, window: "1m" },
};

// Usage
const { requests, window } = RATE_LIMITS["booking-create"];
const result = await rateLimiter.check(
  request,
  "booking-create",
  requests,
  window,
);

if (!result.success) {
  return NextResponse.json(
    {
      error: "Too many requests",
      retryAfter: result.retryAfter,
    },
    {
      status: 429,
      headers: { "Retry-After": String(result.retryAfter) },
    },
  );
}
```

## Error Handling Patterns

### Standard Error Response

```typescript
interface ErrorResponse {
  error: string;
  code?: string;
  details?: Record<string, unknown>;
}

// Usage
return NextResponse.json(
  {
    error: "Resource not found",
    code: "RESOURCE_NOT_FOUND",
  },
  { status: 404 },
);
```

### Error Categories

```typescript
// 400 Bad Request - Invalid input
return NextResponse.json(
  { error: "Invalid input", details: validationErrors },
  { status: 400 },
);

// 401 Unauthorized - Not authenticated
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

// 403 Forbidden - Not authorized for action
return NextResponse.json(
  { error: "Forbidden", code: "INSUFFICIENT_PERMISSIONS" },
  { status: 403 },
);

// 404 Not Found - Resource doesn't exist
return NextResponse.json({ error: "Resource not found" }, { status: 404 });

// 409 Conflict - Duplicate or conflict
return NextResponse.json(
  { error: "Resource already exists", code: "DUPLICATE" },
  { status: 409 },
);

// 429 Too Many Requests - Rate limited
return NextResponse.json(
  { error: "Too many requests", retryAfter: 60 },
  { status: 429 },
);

// 500 Internal Server Error - Unexpected error
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
```

### Try-Catch Pattern

```typescript
export async function POST(request: NextRequest) {
  try {
    // Route logic here
  } catch (error: any) {
    // Log for debugging
    console.error(`POST /api/resource error:`, error);

    // Handle known errors
    if (error.code === "P2002") {
      // Prisma unique constraint
      return NextResponse.json(
        { error: "Resource already exists" },
        { status: 409 },
      );
    }

    if (error.code === "P2025") {
      // Prisma not found
      return NextResponse.json(
        { error: "Resource not found" },
        { status: 404 },
      );
    }

    // Generic error
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 },
    );
  }
}
```

## Test Generation

### Test Template

```typescript
// __tests__/api/resource.test.ts

import { createMocks } from "node-mocks-http";
import { GET, POST } from "@/app/api/resource/route";

// Mock dependencies
jest.mock("@/lib/auth", () => ({
  authOptions: {},
}));

jest.mock("next-auth", () => ({
  getServerSession: jest.fn(),
}));

jest.mock("@/lib/db", () => ({
  prisma: {
    resource: {
      findMany: jest.fn(),
      create: jest.fn(),
    },
  },
}));

describe("GET /api/resource", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("returns 401 without authentication", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue(null);

    const request = new Request("http://localhost/api/resource");
    const response = await GET(request as any);

    expect(response.status).toBe(401);
    const json = await response.json();
    expect(json.error).toBe("Unauthorized");
  });

  it("returns resources for authenticated user", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue({
      user: { id: "user-123" },
    });

    const { prisma } = require("@/lib/db");
    prisma.resource.findMany.mockResolvedValue([
      { id: "1", name: "Test Resource" },
    ]);

    const request = new Request("http://localhost/api/resource");
    const response = await GET(request as any);

    expect(response.status).toBe(200);
    const json = await response.json();
    expect(json.data).toHaveLength(1);
  });
});

describe("POST /api/resource", () => {
  it("returns 400 for invalid input", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue({
      user: { id: "user-123" },
    });

    const request = new Request("http://localhost/api/resource", {
      method: "POST",
      body: JSON.stringify({ name: "" }), // Invalid - empty name
    });

    const response = await POST(request as any);
    expect(response.status).toBe(400);
  });

  it("creates resource with valid input", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue({
      user: { id: "user-123" },
    });

    const { prisma } = require("@/lib/db");
    prisma.resource.create.mockResolvedValue({
      id: "new-id",
      name: "New Resource",
      userId: "user-123",
    });

    const request = new Request("http://localhost/api/resource", {
      method: "POST",
      body: JSON.stringify({
        name: "New Resource",
        email: "test@example.com",
      }),
    });

    const response = await POST(request as any);
    expect(response.status).toBe(201);
  });
});
```

## Webhook Patterns

### Stripe Webhook Example

```typescript
// app/api/stripe/webhook/route.ts

import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { prisma } from "@/lib/db";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature")!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (error: any) {
    console.error("Webhook signature verification failed:", error.message);
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  try {
    switch (event.type) {
      case "checkout.session.completed":
        await handleCheckoutComplete(event.data.object);
        break;
      case "customer.subscription.updated":
        await handleSubscriptionUpdate(event.data.object);
        break;
      case "customer.subscription.deleted":
        await handleSubscriptionCancelled(event.data.object);
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    return NextResponse.json({ received: true });
  } catch (error: any) {
    console.error(`Webhook handler error:`, error);
    return NextResponse.json(
      { error: "Webhook handler failed" },
      { status: 500 },
    );
  }
}

// Handler functions
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
  const userId = session.metadata?.userId;
  if (!userId) return;

  await prisma.subscription.create({
    data: {
      userId,
      stripeSubscriptionId: session.subscription as string,
      status: "active",
    },
  });
}
```

## Database Patterns

### Using Dual Database (Prisma + Supabase)

```typescript
import { saveToBothDatabases } from "@/lib/dual-database";

// For data that needs to sync to both databases
await saveToBothDatabases("users", {
  email: "user@example.com",
  name: "John Doe",
  companyName: "Acme Corp",
});
```

### Prisma Queries

```typescript
import { prisma } from "@/lib/db";

// Find many with filters
const bookings = await prisma.booking.findMany({
  where: {
    userId: session.user.id,
    status: { in: ["pending", "confirmed"] },
    date: { gte: new Date() },
  },
  include: {
    service: true,
    user: { select: { name: true, email: true } },
  },
  orderBy: { date: "asc" },
  take: 10,
});

// Create with relation
const booking = await prisma.booking.create({
  data: {
    date: new Date(body.date),
    serviceId: body.serviceId,
    userId: session.user.id,
    notes: body.notes,
  },
  include: { service: true },
});

// Update
const updated = await prisma.booking.update({
  where: { id: bookingId },
  data: { status: "confirmed" },
});

// Delete
await prisma.booking.delete({
  where: { id: bookingId },
});
```

## Quick Reference

### New Endpoint Checklist

- [ ] Create route file at correct path
- [ ] Add authentication check (if required)
- [ ] Define Zod validation schema
- [ ] Implement rate limiting
- [ ] Add error handling with try-catch
- [ ] Return appropriate status codes
- [ ] Add console.error for debugging
- [ ] Write unit tests
- [ ] Test manually with curl/Postman
- [ ] Update API documentation

### HTTP Status Codes

| Code | Meaning           | Use When                          |
| ---- | ----------------- | --------------------------------- |
| 200  | OK                | Successful GET, PUT, PATCH        |
| 201  | Created           | Successful POST creating resource |
| 204  | No Content        | Successful DELETE                 |
| 400  | Bad Request       | Invalid input/validation failed   |
| 401  | Unauthorized      | Not authenticated                 |
| 403  | Forbidden         | Not authorized for action         |
| 404  | Not Found         | Resource doesn't exist            |
| 409  | Conflict          | Duplicate/conflict                |
| 429  | Too Many Requests | Rate limited                      |
| 500  | Internal Error    | Unexpected server error           |

## See Also

- [create-api-endpoint.ts](./create-api-endpoint.ts) - Programmatic endpoint generator
- [security-audit.ts](./security-audit.ts) - Security scanning
- [dual-database-save.ts](./dual-database-save.ts) - Database sync patterns
