Build or update the BlueBubbles external channel plugin for Moltbot (extension package, REST...
npx skills add soilmass/vibe-coding-plugin --skill "api-routes"
Install specific skill from multi-skill repository
# Description
>
# SKILL.md
name: api-routes
description: >
Next.js 15 App Router route handlers β GET/POST/PUT/DELETE exports, NextRequest/NextResponse, async params, CORS, streaming responses
allowed-tools: Read, Grep, Glob
API Routes
Purpose
Next.js 15 App Router route handler patterns. Covers route.ts exports, request/response
handling, and when to use route handlers vs Server Actions. The ONE skill for HTTP API endpoints.
When to Use
- Creating REST API endpoints for external consumers
- Handling webhooks from third-party services
- Streaming responses (SSE, file downloads)
- Building endpoints consumed by non-React clients
When NOT to Use
- Form submissions from React components β
react-server-actions - Internal data fetching β
nextjs-datawith Server Components - Authentication callbacks β
auth
Pattern
Basic route handler
// app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q");
const products = await db.product.findMany({
where: query ? { name: { contains: query } } : undefined,
});
return NextResponse.json(products);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const product = await db.product.create({ data: body });
return NextResponse.json(product, { status: 201 });
}
Dynamic route (params is a Promise in Next.js 15)
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params; // Must await!
const product = await db.product.findUnique({ where: { id } });
if (!product) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(product);
}
CORS headers
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
Cursor-based pagination
// app/api/posts/route.ts
export async function GET(request: NextRequest) {
const cursor = request.nextUrl.searchParams.get("cursor");
const limit = Math.min(Number(request.nextUrl.searchParams.get("limit") ?? 20), 100);
const posts = await db.post.findMany({
take: limit + 1, // Fetch one extra to check if more exist
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
orderBy: { createdAt: "desc" },
});
const hasMore = posts.length > limit;
const data = hasMore ? posts.slice(0, -1) : posts;
return NextResponse.json({
data,
nextCursor: hasMore ? data[data.length - 1].id : null,
});
}
Idempotency key pattern
// app/api/orders/route.ts
export async function POST(request: NextRequest) {
const idempotencyKey = request.headers.get("Idempotency-Key");
if (!idempotencyKey) {
return NextResponse.json({ error: "Idempotency-Key header required" }, { status: 400 });
}
const existing = await db.order.findUnique({ where: { idempotencyKey } });
if (existing) return NextResponse.json(existing); // Return cached result
const body = await request.json();
const order = await db.order.create({
data: { ...body, idempotencyKey },
});
return NextResponse.json(order, { status: 201 });
}
Anti-pattern
// WRONG: using route handlers for form mutations in React components
// app/api/create-todo/route.ts
export async function POST(req: NextRequest) {
const data = await req.json();
await db.todo.create({ data });
return NextResponse.json({ ok: true });
}
// Client component calling the route handler
// This adds unnecessary network hop β use Server Actions instead
// WRONG: offset pagination on large tables
const posts = await db.post.findMany({
skip: page * 20, // Gets slower as page increases β O(n) skip
take: 20,
});
// Use cursor-based pagination instead (see pattern above)
For React form submissions, Server Actions eliminate the API layer entirely.
Route handlers are for external consumers, webhooks, and non-React clients.
Common Mistakes
- Forgetting to
await paramsβ params is a Promise in Next.js 15 - Missing CORS
OPTIONShandler β preflight requests fail silently - Not returning proper status codes (201 for created, 204 for no content)
- Using route handlers for React form mutations β use Server Actions
- Forgetting to validate request body with Zod
- Offset pagination on large tables β use cursor-based pagination
- No idempotency key for create operations β retries cause duplicates
Checklist
- [ ] Params are awaited before use
- [ ] Request body validated with Zod schema
- [ ] Proper HTTP status codes returned
- [ ] CORS headers set for cross-origin endpoints
- [ ] Error responses use consistent format
- [ ] Pagination uses cursor-based approach for large datasets
- [ ] Create operations support idempotency keys
Composes With
react-server-actionsβ use actions for React mutations, routes for external APIssecurityβ validate auth, rate limit, sanitize inputstypescript-patternsβ type request/response shapesfile-uploadsβ route handlers for upload endpoints and presigned URLsrate-limitingβ protect API routes from abuse with rate limitspaymentsβ webhook route handlers for payment provider callbacksloggingβ structured logging in route handlerswebhooksβ webhook signature verification and event handling
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.