# Next.js API Route Writer

> **Purpose:** Generate a production-ready Next.js 16 App Router API route file following SkillSlap's exact conventions: rate limiting, auth, Supabase server client, typed error responses, and correct HTTP status codes.

---

## Invocation

```
/api-route <description>
```

**Examples:**
- `/api-route POST /api/skill-collections — authenticated user creates a named collection`
- `/api-route GET /api/users/[username]/collections — public endpoint, returns user's public collections`
- `/api-route DELETE /api/skill-collections/[id] — owner can delete their collection`

---

## SkillSlap API Route Conventions

### File location
```
app/api/[resource]/route.ts           -- collection endpoints (GET list, POST create)
app/api/[resource]/[id]/route.ts      -- item endpoints (GET one, PUT update, DELETE)
```

### Standard imports
```ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { checkRateLimit } from '@/lib/rate-limit'
```

### Auth check pattern
```ts
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
```

### Rate limiting — add to ALL mutation endpoints (POST, PUT, PATCH, DELETE)
```ts
// POST endpoints: stricter (5–10 per minute)
const limited = await checkRateLimit(request, { limit: 10, window: 60 })
if (limited) return limited

// PUT/PATCH: moderate (10–20 per minute)
const limited = await checkRateLimit(request, { limit: 20, window: 60 })
if (limited) return limited
```

No rate limiting needed on GET endpoints (Supabase RLS handles access control).

### Input validation
```ts
const body = await request.json()
const { title, skill_id } = body

if (!title || typeof title !== 'string' || title.trim().length === 0) {
  return NextResponse.json({ error: 'title is required' }, { status: 400 })
}
if (title.length > 100) {
  return NextResponse.json({ error: 'title must be 100 characters or less' }, { status: 400 })
}
```

### Supabase query pattern — use typed client with `as any` for joins
```ts
const { data, error } = await (supabase.from('skill_collections') as any)
  .select('id, title, created_at, skill_count')
  .eq('user_id', user.id)
  .order('created_at', { ascending: false })

if (error) {
  console.error('skill_collections fetch error:', error)
  return NextResponse.json({ error: 'Failed to fetch collections' }, { status: 500 })
}
```

### Success responses
```ts
// Created
return NextResponse.json(data, { status: 201 })

// Updated / returned
return NextResponse.json(data)  // defaults to 200

// Deleted (no body)
return new NextResponse(null, { status: 204 })
```

### 404 pattern
```ts
if (!data) {
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
```

### Ownership check (after fetching the resource)
```ts
const { data: collection } = await (supabase.from('skill_collections') as any)
  .select('id, user_id')
  .eq('id', params.id)
  .single()

if (!collection) {
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
if (collection.user_id !== user.id) {
  return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
```

### Full route template
```ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { checkRateLimit } from '@/lib/rate-limit'

export async function POST(request: NextRequest) {
  // 1. Rate limit
  const limited = await checkRateLimit(request, { limit: 10, window: 60 })
  if (limited) return limited

  // 2. Auth
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // 3. Validate input
  const body = await request.json()
  const { title } = body
  if (!title || typeof title !== 'string') {
    return NextResponse.json({ error: 'title is required' }, { status: 400 })
  }

  // 4. Write to DB
  const { data, error } = await (supabase.from('skill_collections') as any)
    .insert({ title: title.trim(), user_id: user.id })
    .select('id, title, created_at')
    .single()

  if (error) {
    console.error('insert error:', error)
    return NextResponse.json({ error: 'Failed to create collection' }, { status: 500 })
  }

  return NextResponse.json(data, { status: 201 })
}
```

---

## HTTP Status Code Guide

| Situation | Status |
|---|---|
| Success, resource created | 201 |
| Success, data returned | 200 |
| Success, no content | 204 |
| Missing required field | 400 |
| Not authenticated | 401 |
| Authenticated but not allowed | 403 |
| Resource not found | 404 |
| Rate limited | 429 (from checkRateLimit) |
| DB error / unexpected | 500 |

---

## Output Format

1. **Route file** — complete `route.ts`, ready to save
2. **File path** — where it belongs in `app/api/`
3. **Summary** — HTTP method, path, auth required, rate limited Y/N
