{"manifest":{"name":"Next.js API Route Writer","version":"1.0.0","description":"Generate Next.js 16 App Router API routes following SkillSlap conventions: rate limiting, Supabase auth check, typed responses, proper error handling. Describe the endpoint and get a production-ready route.ts.","tags":["nextjs","api","typescript","supabase","skillslap"],"standard":"agentskills.io","standard_version":"1.0","content_checksum":"48588d988ca63067bdef45f5ff6b1b91901d40ef94ad3f68cd0c152165a77386","bundle_checksum":null,"metadata":{},"files":[]},"files":{"SKILL.md":"# Next.js API Route Writer\n\n> **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.\n\n---\n\n## Invocation\n\n```\n/api-route <description>\n```\n\n**Examples:**\n- `/api-route POST /api/skill-collections — authenticated user creates a named collection`\n- `/api-route GET /api/users/[username]/collections — public endpoint, returns user's public collections`\n- `/api-route DELETE /api/skill-collections/[id] — owner can delete their collection`\n\n---\n\n## SkillSlap API Route Conventions\n\n### File location\n```\napp/api/[resource]/route.ts           -- collection endpoints (GET list, POST create)\napp/api/[resource]/[id]/route.ts      -- item endpoints (GET one, PUT update, DELETE)\n```\n\n### Standard imports\n```ts\nimport { NextRequest, NextResponse } from 'next/server'\nimport { createClient } from '@/lib/supabase/server'\nimport { checkRateLimit } from '@/lib/rate-limit'\n```\n\n### Auth check pattern\n```ts\nconst supabase = await createClient()\nconst { data: { user } } = await supabase.auth.getUser()\nif (!user) {\n  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n}\n```\n\n### Rate limiting — add to ALL mutation endpoints (POST, PUT, PATCH, DELETE)\n```ts\n// POST endpoints: stricter (5–10 per minute)\nconst limited = await checkRateLimit(request, { limit: 10, window: 60 })\nif (limited) return limited\n\n// PUT/PATCH: moderate (10–20 per minute)\nconst limited = await checkRateLimit(request, { limit: 20, window: 60 })\nif (limited) return limited\n```\n\nNo rate limiting needed on GET endpoints (Supabase RLS handles access control).\n\n### Input validation\n```ts\nconst body = await request.json()\nconst { title, skill_id } = body\n\nif (!title || typeof title !== 'string' || title.trim().length === 0) {\n  return NextResponse.json({ error: 'title is required' }, { status: 400 })\n}\nif (title.length > 100) {\n  return NextResponse.json({ error: 'title must be 100 characters or less' }, { status: 400 })\n}\n```\n\n### Supabase query pattern — use typed client with `as any` for joins\n```ts\nconst { data, error } = await (supabase.from('skill_collections') as any)\n  .select('id, title, created_at, skill_count')\n  .eq('user_id', user.id)\n  .order('created_at', { ascending: false })\n\nif (error) {\n  console.error('skill_collections fetch error:', error)\n  return NextResponse.json({ error: 'Failed to fetch collections' }, { status: 500 })\n}\n```\n\n### Success responses\n```ts\n// Created\nreturn NextResponse.json(data, { status: 201 })\n\n// Updated / returned\nreturn NextResponse.json(data)  // defaults to 200\n\n// Deleted (no body)\nreturn new NextResponse(null, { status: 204 })\n```\n\n### 404 pattern\n```ts\nif (!data) {\n  return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n```\n\n### Ownership check (after fetching the resource)\n```ts\nconst { data: collection } = await (supabase.from('skill_collections') as any)\n  .select('id, user_id')\n  .eq('id', params.id)\n  .single()\n\nif (!collection) {\n  return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\nif (collection.user_id !== user.id) {\n  return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n}\n```\n\n### Full route template\n```ts\nimport { NextRequest, NextResponse } from 'next/server'\nimport { createClient } from '@/lib/supabase/server'\nimport { checkRateLimit } from '@/lib/rate-limit'\n\nexport async function POST(request: NextRequest) {\n  // 1. Rate limit\n  const limited = await checkRateLimit(request, { limit: 10, window: 60 })\n  if (limited) return limited\n\n  // 2. Auth\n  const supabase = await createClient()\n  const { data: { user } } = await supabase.auth.getUser()\n  if (!user) {\n    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n  }\n\n  // 3. Validate input\n  const body = await request.json()\n  const { title } = body\n  if (!title || typeof title !== 'string') {\n    return NextResponse.json({ error: 'title is required' }, { status: 400 })\n  }\n\n  // 4. Write to DB\n  const { data, error } = await (supabase.from('skill_collections') as any)\n    .insert({ title: title.trim(), user_id: user.id })\n    .select('id, title, created_at')\n    .single()\n\n  if (error) {\n    console.error('insert error:', error)\n    return NextResponse.json({ error: 'Failed to create collection' }, { status: 500 })\n  }\n\n  return NextResponse.json(data, { status: 201 })\n}\n```\n\n---\n\n## HTTP Status Code Guide\n\n| Situation | Status |\n|---|---|\n| Success, resource created | 201 |\n| Success, data returned | 200 |\n| Success, no content | 204 |\n| Missing required field | 400 |\n| Not authenticated | 401 |\n| Authenticated but not allowed | 403 |\n| Resource not found | 404 |\n| Rate limited | 429 (from checkRateLimit) |\n| DB error / unexpected | 500 |\n\n---\n\n## Output Format\n\n1. **Route file** — complete `route.ts`, ready to save\n2. **File path** — where it belongs in `app/api/`\n3. **Summary** — HTTP method, path, auth required, rate limited Y/N\n"}}