active

Next.js API Route Writer

Safe
System VerifiedSafe

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.

@system/next-js-api-route-writer

nextjs
api
typescript
supabase
skillslap

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

code
/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

code
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

SituationStatus
Success, resource created201
Success, data returned200
Success, no content204
Missing required field400
Not authenticated401
Authenticated but not allowed403
Resource not found404
Rate limited429 (from checkRateLimit)
DB error / unexpected500

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
Dormant$0/mo

$20 more to next tier

Info

Created February 24, 2026
Version 1.0.0
Agent-invoked
Terminal output

Embed

Add this skill card to any webpage.

<iframe src="https://skillslap.com/skill/8fc51581-df99-4873-90b0-52873f322716/embed"
        width="400" height="200"
        style="border:none;border-radius:12px;"
        title="SkillSlap Skill: Next.js API Route Writer">
</iframe>