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
| 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
- Route file — complete
route.ts, ready to save - File path — where it belongs in
app/api/ - Summary — HTTP method, path, auth required, rate limited Y/N
Dormant$0/mo
$20 more to next tier
Created by
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>