api-route-conventions
Expert knowledge on Next.js API route patterns, authentication with getAuthOrTest, error handling, response formats, rate limiting, and webhook verification. Use this skill when user asks about "create api endpoint", "api route", "error handling", "authentication", "next.js api", or "route handler".
Best use case
api-route-conventions is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Expert knowledge on Next.js API route patterns, authentication with getAuthOrTest, error handling, response formats, rate limiting, and webhook verification. Use this skill when user asks about "create api endpoint", "api route", "error handling", "authentication", "next.js api", or "route handler".
Teams using api-route-conventions should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/api-route-conventions/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How api-route-conventions Compares
| Feature / Agent | api-route-conventions | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Expert knowledge on Next.js API route patterns, authentication with getAuthOrTest, error handling, response formats, rate limiting, and webhook verification. Use this skill when user asks about "create api endpoint", "api route", "error handling", "authentication", "next.js api", or "route handler".
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# API Route Conventions Expert
You are an expert in Next.js API route conventions for this platform. This skill provides templates, patterns, and best practices for creating consistent, secure API endpoints.
## When To Use This Skill
This skill activates when users:
- Need to create a new API endpoint
- Debug authentication issues in routes
- Implement error handling patterns
- Work with webhook endpoints (Stripe, Clerk, QStash)
- Need consistent response formats
- Implement rate limiting or validation
- Convert old API routes to new patterns
## Core Knowledge
### Standard API Route Template
**Location:** `/app/api/[feature]/route.ts`
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { getAuthOrTest } from '@/lib/auth/get-auth-or-test';
import { logger, LogCategory } from '@/lib/logging';
import { db } from '@/lib/db';
export async function GET(req: NextRequest) {
try {
// 1. Authentication
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 2. Parse query parameters
const searchParams = req.nextUrl.searchParams;
const param = searchParams.get('param');
// 3. Validate input
if (!param) {
return NextResponse.json(
{ error: 'Missing required parameter: param' },
{ status: 400 }
);
}
// 4. Business logic
const data = await db.query.someTable.findMany({
where: eq(someTable.userId, auth.userId)
});
// 5. Success response
return NextResponse.json({
success: true,
data,
meta: {
count: data.length,
timestamp: new Date().toISOString()
}
});
} catch (error) {
// 6. Error handling
logger.error('Failed to process request', error, {
endpoint: '/api/feature',
userId: auth?.userId
}, LogCategory.API);
return NextResponse.json(
{
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}
export async function POST(req: NextRequest) {
try {
// 1. Authentication
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// 2. Parse body
const body = await req.json();
// 3. Validate input
if (!body.name || !body.type) {
return NextResponse.json(
{ error: 'Missing required fields: name, type' },
{ status: 400 }
);
}
// 4. Business logic with logging
logger.info('Creating resource', {
userId: auth.userId,
name: body.name
}, LogCategory.API);
const [resource] = await db.insert(someTable)
.values({
userId: auth.userId,
name: body.name,
type: body.type
})
.returning();
// 5. Success response
return NextResponse.json({
success: true,
data: resource
}, { status: 201 });
} catch (error) {
logger.error('Failed to create resource', error, {
endpoint: '/api/feature'
}, LogCategory.API);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
```
### Authentication Patterns
**Primary Auth:** `/lib/auth/get-auth-or-test.ts`
```typescript
import { getAuthOrTest } from '@/lib/auth/get-auth-or-test';
// Standard pattern
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// With email access
const auth = await getAuthOrTest();
const email = auth?.sessionClaims?.email as string | undefined;
```
**Auth Resolution Order:**
1. Test headers (`x-test-user-id`, `x-test-email`)
2. Dev bypass header (`x-dev-auth: dev-bypass`)
3. Environment bypass (`ENABLE_AUTH_BYPASS=true`)
4. Clerk `backendAuth()`
**Dev Bypass Methods:**
1. **Header-Based:**
```bash
curl http://localhost:3000/api/endpoint \
-H "x-dev-auth: dev-bypass" \
-H "x-dev-user-id: user_xxx"
```
2. **Environment-Based:**
```bash
# .env.local
ENABLE_AUTH_BYPASS=true
AUTH_BYPASS_USER_ID=user_xxx
```
3. **Test Headers:**
```bash
curl http://localhost:3000/api/endpoint \
-H "x-test-user-id: user_xxx" \
-H "x-test-email: test@example.com"
```
### Response Formats
**Success Response:**
```typescript
return NextResponse.json({
success: true,
data: result,
meta: {
count: result.length,
page: 1,
timestamp: new Date().toISOString()
}
}, { status: 200 });
```
**Error Response:**
```typescript
return NextResponse.json({
error: 'Error message',
code: 'ERROR_CODE', // Optional
details: { /* ... */ } // Optional
}, { status: 400 });
```
**Status Codes:**
- `200` - Success (GET, PUT, DELETE)
- `201` - Created (POST)
- `400` - Bad Request (validation failed)
- `401` - Unauthorized (no auth)
- `403` - Forbidden (auth but no permission, e.g., plan limits)
- `404` - Not Found
- `409` - Conflict (duplicate resource)
- `429` - Too Many Requests (rate limit)
- `500` - Internal Server Error
### Webhook Pattern
**Stripe Webhook:** `/app/api/stripe/webhook/route.ts`
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { StripeService } from '@/lib/stripe/stripe-service';
export async function POST(req: NextRequest) {
try {
// 1. Get raw body (required for signature verification)
const body = await req.text();
const signature = req.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json(
{ error: 'No signature provided' },
{ status: 400 }
);
}
// 2. Verify signature
const event = StripeService.validateWebhookSignature(body, signature);
// 3. Handle event
switch (event.type) {
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
// ... other cases
default:
logger.info('Unhandled webhook event', {
type: event.type
});
}
// 4. Always return 200 (even for unhandled events)
return NextResponse.json({ received: true });
} catch (error) {
logger.error('Webhook error', error);
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
);
}
}
```
**QStash Webhook:** `/app/api/qstash/*/route.ts`
```typescript
import { Receiver } from '@upstash/qstash';
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get('Upstash-Signature');
// Verify signature (skip in dev if configured)
if (shouldVerifySignature()) {
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 401 });
}
const valid = await receiver.verify({
signature,
body: rawBody,
url: callbackUrl
});
if (!valid) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
}
// Process webhook...
}
```
### Validation Patterns
**Zod Schema Validation:**
```typescript
import { z } from 'zod';
const CreateCampaignSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
searchType: z.enum(['instagram-reels', 'tiktok-keyword', 'youtube-keyword']),
keywords: z.array(z.string()).min(1).max(10)
});
export async function POST(req: NextRequest) {
const body = await req.json();
// Validate
const validation = CreateCampaignSchema.safeParse(body);
if (!validation.success) {
return NextResponse.json({
error: 'Validation failed',
details: validation.error.issues
}, { status: 400 });
}
const data = validation.data;
// Use validated data...
}
```
**Manual Validation:**
```typescript
function validateInput(data: any): { valid: boolean; error?: string } {
if (!data.name || typeof data.name !== 'string') {
return { valid: false, error: 'Invalid name' };
}
if (data.name.length > 100) {
return { valid: false, error: 'Name too long' };
}
return { valid: true };
}
const validation = validateInput(body);
if (!validation.valid) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
```
### Plan Enforcement Integration
```typescript
import { PlanEnforcementService } from '@/lib/services/plan-enforcement';
export async function POST(req: NextRequest) {
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Validate plan limits BEFORE action
const validation = await PlanEnforcementService.validateCampaignCreation(
auth.userId
);
if (!validation.allowed) {
return NextResponse.json({
error: validation.reason,
usage: validation.usage,
upgrade_required: true
}, { status: 403 });
}
// Create resource...
// Track usage AFTER success
await PlanEnforcementService.trackCampaignCreated(auth.userId);
return NextResponse.json({ success: true });
}
```
## Common Patterns
### Pattern 1: Dynamic Route with ID
```typescript
// app/api/campaigns/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const campaignId = params.id;
const campaign = await db.query.campaigns.findFirst({
where: and(
eq(campaigns.id, campaignId),
eq(campaigns.userId, auth.userId) // Security: user can only access own data
)
});
if (!campaign) {
return NextResponse.json({ error: 'Campaign not found' }, { status: 404 });
}
return NextResponse.json({ data: campaign });
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Verify ownership before delete
const campaign = await db.query.campaigns.findFirst({
where: and(
eq(campaigns.id, params.id),
eq(campaigns.userId, auth.userId)
)
});
if (!campaign) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
await db.delete(campaigns).where(eq(campaigns.id, params.id));
return NextResponse.json({ success: true });
}
```
### Pattern 2: Pagination
```typescript
export async function GET(req: NextRequest) {
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const searchParams = req.nextUrl.searchParams;
const page = parseInt(searchParams.get('page') || '1');
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100);
const offset = (page - 1) * limit;
const [items, [{ total }]] = await Promise.all([
db.query.campaigns.findMany({
where: eq(campaigns.userId, auth.userId),
orderBy: [desc(campaigns.createdAt)],
limit,
offset
}),
db.select({ total: count() })
.from(campaigns)
.where(eq(campaigns.userId, auth.userId))
]);
return NextResponse.json({
data: items,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
}
```
### Pattern 3: Admin-Only Endpoint
```typescript
import { isAdmin } from '@/lib/auth/admin-utils';
export async function POST(req: NextRequest) {
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check admin status
if (!await isAdmin(auth.userId)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Admin-only logic...
}
```
## Anti-Patterns (Avoid These)
### Anti-Pattern 1: No Auth Check
```typescript
// BAD: Anyone can access
export async function POST(req: NextRequest) {
const body = await req.json();
await db.insert(campaigns).values(body);
return NextResponse.json({ success: true });
}
```
**Do this instead:**
```typescript
// GOOD: Always check auth
export async function POST(req: NextRequest) {
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// ...
}
```
### Anti-Pattern 2: Exposing Internal Errors
```typescript
// BAD: Exposes stack traces and DB details
catch (error) {
return NextResponse.json({ error: error.toString() }, { status: 500 });
}
```
**Do this instead:**
```typescript
// GOOD: Log full error, return safe message
catch (error) {
logger.error('Operation failed', error, { userId: auth?.userId });
return NextResponse.json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? error.message : undefined
}, { status: 500 });
}
```
### Anti-Pattern 3: No Input Validation
```typescript
// BAD: Trusting user input
const { name, email } = await req.json();
await db.insert(users).values({ name, email });
```
**Do this instead:**
```typescript
// GOOD: Validate before using
const body = await req.json();
if (!body.name || typeof body.name !== 'string' || body.name.length > 100) {
return NextResponse.json({ error: 'Invalid name' }, { status: 400 });
}
```
## Related Files
- `/lib/auth/get-auth-or-test.ts` - Authentication resolver
- `/lib/auth/admin-utils.ts` - Admin check
- `/lib/services/plan-enforcement.ts` - Plan validation
- `/lib/logging/index.ts` - Structured logging
- `/app/api/campaigns/route.ts` - Example CRUD endpoint
- `/app/api/stripe/webhook/route.ts` - Webhook pattern
- `/app/api/qstash/process-search/route.ts` - QStash pattern
## Testing API Endpoints
**Test with curl:**
```bash
# With dev bypass
curl -X POST http://localhost:3000/api/campaigns \
-H "x-dev-auth: dev-bypass" \
-H "x-dev-user-id: user_xxx" \
-H "Content-Type: application/json" \
-d '{"name":"Test Campaign","searchType":"instagram-reels"}'
# With Clerk session (production)
curl http://localhost:3000/api/campaigns \
-H "Authorization: Bearer $CLERK_SESSION_TOKEN"
```
**Test script:**
```bash
node scripts/simple-api-logger.js
```Related Skills
entity-class-conventions
Sets the standards for entity class design including annotations, ID generation strategies, and relationship configurations for database interaction.
add-route-context
为Flutter页面添加路由上下文记录功能,支持日期等参数的AI上下文识别。当需要让AI助手通过"询问当前上下文"功能获取页面状态(如日期、ID等参数)时使用。适用场景:(1) 日期驱动的页面(日记、活动、日历等),(2) ID驱动的页面(用户详情、订单详情等),(3) 任何需要AI理解当前页面参数的场景
restcontroller-conventions
Specifies standards for RestController classes, including API route mappings, HTTP method annotations, dependency injection, and error handling with ApiResponse and GlobalExceptionHandler.
fastapi-router-py
Create FastAPI routers with CRUD operations, authentication dependencies, and proper response models. Use when building REST API endpoints, creating new routes, implementing CRUD operations, or add...
commit-conventions
This skill should be used when writing commit messages, when asked about commit format, when reviewing a commit message, or when creating a git commit. Provides Conventional Commits format and project-specific conventions.
api-routes
Generate secure TanStack Start API routes with authentication, rate limiting, validation, and proper error handling. Use when creating API endpoints, REST resources, or backend logic.
api-route
Create a new Next.js API route with x402 micropayment middleware
api-route-scaffold
Create new Next.js API routes following project patterns. Use when user mentions "new endpoint", "add API", "create route", or "POST/GET handler".
api-route-creator
Creates Next.js 16 API routes with auth, validation, and tenant scoping. Use when creating API endpoints.
agentbox-openrouter
Set up OpenRouter as your LLM provider. Guides through account creation, API key setup, config, and making it the default model. Use when a user wants to use OpenRouter models like Claude Sonnet 4.5.
bgo
Automates the complete Blender build-go workflow, from building and packaging your extension/add-on to removing old versions, installing, enabling, and launching Blender for quick testing and iteration.
hire
Interactive hiring wizard to set up a new AI team member. Guides the user through role design via conversation, generates agent identity files, and optionally sets up performance reviews. Use when the user wants to hire, add, or set up a new AI agent, team member, or assistant. Triggers on phrases like "hire", "add an agent", "I need help with X" (implying a new role), or "/hire".