xiaohongshu
xiaohongshu
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".
Full skill instructions
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.
This skill activates when users:
Location: /app/api/[feature]/route.ts
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 }
);
}
}
Primary Auth: /lib/auth/get-auth-or-test.ts
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:
x-test-user-id, x-test-email)x-dev-auth: dev-bypass)ENABLE_AUTH_BYPASS=true)backendAuth()Dev Bypass Methods:
curl http://localhost:3000/api/endpoint \
-H "x-dev-auth: dev-bypass" \
-H "x-dev-user-id: user_xxx"
# .env.local
ENABLE_AUTH_BYPASS=true
AUTH_BYPASS_USER_ID=user_xxx
curl http://localhost:3000/api/endpoint \
-H "x-test-user-id: user_xxx" \
-H "x-test-email: [email protected]"
Success Response:
return NextResponse.json({
success: true,
data: result,
meta: {
count: result.length,
page: 1,
timestamp: new Date().toISOString()
}
}, { status: 200 });
Error Response:
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 Found409 - Conflict (duplicate resource)429 - Too Many Requests (rate limit)500 - Internal Server ErrorStripe Webhook: /app/api/stripe/webhook/route.ts
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
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...
}
Zod Schema Validation:
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:
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 });
}
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 });
}
// 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 });
}
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)
}
});
}
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...
}
// 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:
// GOOD: Always check auth
export async function POST(req: NextRequest) {
const auth = await getAuthOrTest();
if (!auth?.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// ...
}
// BAD: Exposes stack traces and DB details
catch (error) {
return NextResponse.json({ error: error.toString() }, { status: 500 });
}
Do this instead:
// 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 });
}
// BAD: Trusting user input
const { name, email } = await req.json();
await db.insert(users).values({ name, email });
Do this instead:
// 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 });
}
/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 patternTest with curl:
# 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:
node scripts/simple-api-logger.js
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience