API Design Patterns & Standards
🚀 Universal API Architecture
MyStoryFlow’s API design follows RESTful principles with Next.js 15 App Router patterns, ensuring consistent, performant, and secure endpoints across all tools. Every API follows identical patterns for maximum maintainability and developer experience.
📋 Universal Response Format
Standard Response Interface
Every API endpoint returns responses in this exact format:
interface UniversalApiResponse<T = any> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: any;
field?: string; // For validation errors
timestamp: string;
};
meta?: {
timestamp: string;
requestId: string;
processingTime: number;
pagination?: PaginationMeta;
rateLimit?: RateLimitMeta;
conversionOpportunity?: boolean;
debugInfo?: any; // Only in development
};
}
interface PaginationMeta {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
interface RateLimitMeta {
limit: number;
remaining: number;
resetTime: number;
retryAfter?: number;
}Success Response Example
{
"success": true,
"data": {
"id": "prompt-romance-sunset-7k2m",
"title": "Forbidden Love in Victorian England",
"content": {
"prompt": "Your character discovers...",
"analysis": "This prompt explores...",
"variants": [...]
},
"shareCode": "romance-sunset-7k2m"
},
"meta": {
"timestamp": "2024-07-21T10:30:45Z",
"requestId": "req_abc123def456",
"processingTime": 2847,
"conversionOpportunity": false
}
}Error Response Example
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Generation limit reached. Try again in 1 minute.",
"details": {
"currentUsage": 5,
"dailyLimit": 5,
"resetTime": 1642780800
},
"timestamp": "2024-07-21T10:30:45Z"
},
"meta": {
"timestamp": "2024-07-21T10:30:45Z",
"requestId": "req_abc123def456",
"processingTime": 45,
"rateLimit": {
"limit": 5,
"remaining": 0,
"resetTime": 1642780800,
"retryAfter": 60
},
"conversionOpportunity": true
}
}🛣️ Standard API Routes Structure
Core Routes Pattern
Every tool implements these exact route patterns:
/api/{tool}/
├── generate/ # AI content generation
│ └── route.ts # POST - Generate new content
├── route.ts # GET - Browse/list content, POST - Create manual content
├── [id]/ # Individual content operations
│ ├── route.ts # GET - Fetch, PATCH - Update, DELETE - Remove
│ ├── responses/ # User response system
│ │ └── route.ts # GET - List responses, POST - Create response
│ ├── export/ # Export functionality
│ │ └── route.ts # POST - Generate exports
│ └── analytics/ # Usage analytics
│ └── route.ts # POST - Track events
├── share/ # Sharing system
│ ├── route.ts # POST - Create share links
│ └── [shareCode]/ # Shared content access
│ └── route.ts # GET - View shared content
├── browse/ # Public browsing (optional dedicated endpoint)
│ └── route.ts # GET - Advanced browse with filters
└── admin/ # Admin operations (protected)
├── featured/ # Feature management
│ └── route.ts # POST - Feature/unfeature content
├── moderate/ # Content moderation
│ └── route.ts # POST - Approve/reject content
└── analytics/ # Admin analytics
└── route.ts # GET - Detailed analytics🤖 AI Generation Endpoint Pattern
POST /api/{tool}/generate
The core AI generation endpoint follows this exact implementation:
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { trackAIUsage, getRecommendedToolsModel } from '@/lib/ai-usage-tracker'
import { checkRateLimit } from '@/lib/rate-limiter'
import { ToolAI } from '@/lib/ai/{tool}-ai'
interface GenerateRequestBody {
sessionId: string
options: ToolSpecificOptions
humanVerification?: string
}
export async function POST(request: NextRequest) {
const startTime = Date.now()
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
try {
// ===========================================
// 1. REQUEST VALIDATION & PARSING
// ===========================================
const body: GenerateRequestBody = await request.json()
if (!body.sessionId) {
return NextResponse.json({
success: false,
error: {
code: 'MISSING_SESSION_ID',
message: 'Session ID is required',
field: 'sessionId',
timestamp: new Date().toISOString()
},
meta: {
timestamp: new Date().toISOString(),
requestId,
processingTime: Date.now() - startTime
}
}, { status: 400 })
}
// ===========================================
// 2. RATE LIMITING & SECURITY
// ===========================================
const rateLimitResult = await checkRateLimit(request, '{tool}_generation')
if (!rateLimitResult.allowed) {
return NextResponse.json({
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: rateLimitResult.message,
details: rateLimitResult.details,
timestamp: new Date().toISOString()
},
meta: {
timestamp: new Date().toISOString(),
requestId,
processingTime: Date.now() - startTime,
rateLimit: {
limit: rateLimitResult.limit,
remaining: rateLimitResult.remaining,
resetTime: rateLimitResult.resetTime,
retryAfter: rateLimitResult.retryAfter
},
conversionOpportunity: true
}
}, {
status: 429,
headers: {
'Retry-After': rateLimitResult.retryAfter?.toString() || '60'
}
})
}
// ===========================================
// 3. AI GENERATION WITH TRACKING
// ===========================================
const supabase = await createClient()
const toolAI = new ToolAI()
const generationResult = await toolAI.generateContent(body.options, {
sessionId: body.sessionId,
requestId
})
// Track AI usage immediately
await trackAIUsage({
sessionId: body.sessionId,
featureName: `[Tools] {tool}_generation`,
promptName: `{tool}_creation_v1`,
modelUsed: getRecommendedToolsModel(),
provider: 'openai',
tokensConsumed: generationResult.usage.total_tokens,
costUsd: generationResult.estimatedCost,
responseTimeMs: generationResult.processingTime,
requestSuccess: true,
metadata: {
toolType: '{tool}',
requestId,
optionsProvided: Object.keys(body.options),
variantGenerated: generationResult.variantType
}
})
// ===========================================
// 4. CONTENT PERSISTENCE
// ===========================================
const shareCode = await generateUniqueShareCode('{tool}')
const { data: savedContent, error: saveError } = await supabase
.from('tools_{tool}')
.insert({
title: generationResult.title,
share_code: shareCode,
session_id: body.sessionId,
content: generationResult.content,
options: body.options,
ai_generation_prompt: generationResult.promptUsed,
ai_tokens_used: generationResult.usage.total_tokens,
ai_cost_cents: Math.round(generationResult.estimatedCost * 100),
ai_model: generationResult.model,
ai_generation_time_ms: generationResult.processingTime,
ai_confidence_score: generationResult.confidenceScore,
seo_title: generationResult.seoMetadata.title,
seo_description: generationResult.seoMetadata.description,
keywords: generationResult.seoMetadata.keywords
})
.select()
.single()
if (saveError) {
// Track failed save but successful generation
await trackAIUsage({
sessionId: body.sessionId,
featureName: `[Tools] {tool}_save_error`,
promptName: `{tool}_creation_v1`,
modelUsed: getRecommendedToolsModel(),
requestSuccess: false,
metadata: {
error: saveError.message,
requestId
}
})
return NextResponse.json({
success: false,
error: {
code: 'SAVE_FAILED',
message: 'Content generated successfully but failed to save',
details: { saveError: saveError.message },
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
// ===========================================
// 5. SUCCESS RESPONSE WITH CORS
// ===========================================
const response = NextResponse.json({
success: true,
data: {
id: savedContent.id,
shareCode: savedContent.share_code,
title: savedContent.title,
content: savedContent.content,
analysis: generationResult.analysis,
seoMetadata: generationResult.seoMetadata,
aiMetadata: {
model: generationResult.model,
tokensUsed: generationResult.usage.total_tokens,
processingTime: generationResult.processingTime,
confidenceScore: generationResult.confidenceScore
}
},
meta: {
timestamp: new Date().toISOString(),
requestId,
processingTime: Date.now() - startTime,
rateLimit: {
limit: rateLimitResult.limit,
remaining: rateLimitResult.remaining - 1,
resetTime: rateLimitResult.resetTime
},
conversionOpportunity: shouldShowConversionCTA(body.sessionId, rateLimitResult)
}
})
// CORS headers for web app integration
response.headers.set('Access-Control-Allow-Origin', process.env.WEB_APP_URL || 'https://mystoryflow.com')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-ID')
response.headers.set('Access-Control-Allow-Credentials', 'true')
return response
} catch (error) {
// ===========================================
// ERROR HANDLING & TRACKING
// ===========================================
console.error(`{tool} generation error:`, error)
// Track failed request
if (body?.sessionId) {
await trackAIUsage({
sessionId: body.sessionId,
featureName: `[Tools] {tool}_generation_error`,
requestSuccess: false,
metadata: {
error: error.message,
stack: error.stack,
requestId
}
}).catch(console.error) // Don't let tracking errors crash the response
}
return NextResponse.json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred. Please try again.',
timestamp: new Date().toISOString()
},
meta: {
timestamp: new Date().toISOString(),
requestId,
processingTime: Date.now() - startTime
}
}, { status: 500 })
}
}
// ===========================================
// CORS PREFLIGHT HANDLING
// ===========================================
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': process.env.WEB_APP_URL || 'https://mystoryflow.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Session-ID',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400'
}
})
}📋 Browse & Filter Endpoint Pattern
GET /api/{tool}
Content browsing and filtering with advanced query support:
import { NextRequest, NextResponse } from 'next/server'
interface BrowseQuery {
page?: string
limit?: string
sort?: 'newest' | 'popular' | 'featured' | 'rating'
filter?: string[]
search?: string
public?: 'true' | 'false'
featured?: 'true' | 'false'
}
export async function GET(request: NextRequest) {
const startTime = Date.now()
const requestId = `browse_${Date.now()}`
try {
// ===========================================
// 1. QUERY PARAMETER PARSING
// ===========================================
const { searchParams } = new URL(request.url)
const query: BrowseQuery = {
page: searchParams.get('page') || '1',
limit: searchParams.get('limit') || '20',
sort: (searchParams.get('sort') as any) || 'newest',
filter: searchParams.getAll('filter'),
search: searchParams.get('search') || undefined,
public: searchParams.get('public') as any,
featured: searchParams.get('featured') as any
}
// Validate pagination
const page = Math.max(1, parseInt(query.page))
const limit = Math.min(100, Math.max(1, parseInt(query.limit)))
const offset = (page - 1) * limit
// ===========================================
// 2. BUILD SUPABASE QUERY
// ===========================================
const supabase = await createClient()
let queryBuilder = supabase
.from('tools_{tool}')
.select(`
id,
title,
share_code,
content,
seo_title,
seo_description,
keywords,
view_count,
share_count,
response_count,
average_rating,
created_at,
updated_at
`, { count: 'exact' })
// Apply filters
if (query.public === 'true') {
queryBuilder = queryBuilder.eq('is_public', true)
}
if (query.featured === 'true') {
queryBuilder = queryBuilder.eq('is_featured', true)
}
// Always filter out archived content for public browsing
queryBuilder = queryBuilder.eq('is_archived', false)
// Search functionality
if (query.search) {
queryBuilder = queryBuilder.or(`
title.ilike.%${query.search}%,
seo_description.ilike.%${query.search}%,
keywords.cs.{${query.search}}
`)
}
// Sorting
switch (query.sort) {
case 'popular':
queryBuilder = queryBuilder.order('view_count', { ascending: false })
break
case 'rating':
queryBuilder = queryBuilder.order('average_rating', { ascending: false })
break
case 'featured':
queryBuilder = queryBuilder.order('is_featured', { ascending: false })
.order('created_at', { ascending: false })
break
case 'newest':
default:
queryBuilder = queryBuilder.order('created_at', { ascending: false })
break
}
// Apply pagination
queryBuilder = queryBuilder.range(offset, offset + limit - 1)
// ===========================================
// 3. EXECUTE QUERY
// ===========================================
const { data, error, count } = await queryBuilder
if (error) {
return NextResponse.json({
success: false,
error: {
code: 'QUERY_ERROR',
message: 'Failed to fetch content',
details: { error: error.message },
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
// ===========================================
// 4. RESPONSE WITH PAGINATION
// ===========================================
const totalPages = Math.ceil((count || 0) / limit)
return NextResponse.json({
success: true,
data: data || [],
meta: {
timestamp: new Date().toISOString(),
requestId,
processingTime: Date.now() - startTime,
pagination: {
page,
limit,
total: count || 0,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
}
})
} catch (error) {
console.error(`{tool} browse error:`, error)
return NextResponse.json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Failed to browse content',
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
}💬 User Response System Pattern
POST /api/{tool}/[id]/responses
User-generated responses to tool content:
interface CreateResponseBody {
sessionId: string
title: string
content: string
isPublic?: boolean
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const startTime = Date.now()
const { id } = await params
try {
const body: CreateResponseBody = await request.json()
// Validation
if (!body.sessionId || !body.title?.trim() || !body.content?.trim()) {
return NextResponse.json({
success: false,
error: {
code: 'INVALID_INPUT',
message: 'Title and content are required',
timestamp: new Date().toISOString()
}
}, { status: 400 })
}
// Rate limiting for responses
const rateLimitResult = await checkRateLimit(request, '{tool}_response')
if (!rateLimitResult.allowed) {
return NextResponse.json({
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many responses. Please wait before adding another.',
timestamp: new Date().toISOString()
}
}, { status: 429 })
}
const supabase = await createClient()
// Verify parent content exists
const { data: parentContent, error: parentError } = await supabase
.from('tools_{tool}')
.select('id, title')
.eq('id', id)
.single()
if (parentError || !parentContent) {
return NextResponse.json({
success: false,
error: {
code: 'PARENT_NOT_FOUND',
message: 'Original content not found',
timestamp: new Date().toISOString()
}
}, { status: 404 })
}
// Create response
const { data: response, error } = await supabase
.from('tools_{tool}_responses')
.insert({
{tool}_id: id,
session_id: body.sessionId,
title: body.title.trim(),
content: body.content.trim(),
is_public: body.isPublic || false,
moderation_status: 'pending'
})
.select()
.single()
if (error) {
return NextResponse.json({
success: false,
error: {
code: 'SAVE_FAILED',
message: 'Failed to save response',
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
return NextResponse.json({
success: true,
data: {
id: response.id,
title: response.title,
content: response.content,
wordCount: response.word_count,
createdAt: response.created_at
},
meta: {
timestamp: new Date().toISOString(),
processingTime: Date.now() - startTime
}
})
} catch (error) {
console.error(`{tool} response creation error:`, error)
return NextResponse.json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Failed to create response',
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
}📄 Export System Pattern
POST /api/{tool}/[id]/export
Multi-format content export:
interface ExportRequestBody {
format: 'json' | 'csv' | 'pdf' | 'html' | 'docx'
options?: {
includeMetadata?: boolean
includeResponses?: boolean
theme?: 'light' | 'dark'
template?: string
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const startTime = Date.now()
const { id } = await params
try {
const body: ExportRequestBody = await request.json()
if (!['json', 'csv', 'pdf', 'html', 'docx'].includes(body.format)) {
return NextResponse.json({
success: false,
error: {
code: 'INVALID_FORMAT',
message: 'Supported formats: json, csv, pdf, html, docx',
timestamp: new Date().toISOString()
}
}, { status: 400 })
}
const supabase = await createClient()
// Fetch content with optional responses
let query = supabase
.from('tools_{tool}')
.select(`
*,
${body.options?.includeResponses ?
`responses:tools_${tool}_responses(*)` :
''
}
`)
.eq('id', id)
.single()
const { data: content, error } = await query
if (error || !content) {
return NextResponse.json({
success: false,
error: {
code: 'CONTENT_NOT_FOUND',
message: 'Content not found or access denied',
timestamp: new Date().toISOString()
}
}, { status: 404 })
}
// Generate export based on format
const exportService = new ExportService()
const exportResult = await exportService.generate({
content,
format: body.format,
options: body.options || {}
})
// Track export analytics
await supabase
.from('tools_{tool}')
.update({ export_count: content.export_count + 1 })
.eq('id', id)
// Return appropriate response based on format
const response = new NextResponse(exportResult.buffer, {
status: 200,
headers: {
'Content-Type': exportResult.mimeType,
'Content-Disposition': `attachment; filename="${exportResult.filename}"`,
'Content-Length': exportResult.buffer.length.toString()
}
})
return response
} catch (error) {
console.error(`{tool} export error:`, error)
return NextResponse.json({
success: false,
error: {
code: 'EXPORT_FAILED',
message: 'Failed to generate export',
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
}🔗 Share System Pattern
GET /api/{tool}/share/[shareCode]
Public sharing with SEO optimization:
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ shareCode: string }> }
) {
const startTime = Date.now()
const { shareCode } = await params
try {
const supabase = await createClient()
// Fetch shared content
const { data: content, error } = await supabase
.from('tools_{tool}')
.select(`
*,
responses:tools_{tool}_responses!left(
id,
title,
content,
word_count,
created_at,
is_public
)
`)
.eq('share_code', shareCode)
.eq('is_archived', false)
.single()
if (error || !content) {
return NextResponse.json({
success: false,
error: {
code: 'CONTENT_NOT_FOUND',
message: 'Shared content not found or has expired',
timestamp: new Date().toISOString()
}
}, { status: 404 })
}
// Update view count asynchronously
supabase
.from('tools_{tool}')
.update({
view_count: content.view_count + 1,
last_accessed_at: new Date().toISOString()
})
.eq('id', content.id)
.then(() => {}) // Fire and forget
// Return public data only
const publicData = {
id: content.id,
shareCode: content.share_code,
title: content.title,
content: content.content,
seoMetadata: {
title: content.seo_title,
description: content.seo_description,
keywords: content.keywords
},
analytics: {
viewCount: content.view_count + 1,
shareCount: content.share_count,
responseCount: content.response_count,
averageRating: content.average_rating
},
responses: content.responses?.filter(r => r.is_public) || [],
createdAt: content.created_at
}
return NextResponse.json({
success: true,
data: publicData,
meta: {
timestamp: new Date().toISOString(),
processingTime: Date.now() - startTime,
conversionOpportunity: true
}
})
} catch (error) {
console.error(`{tool} share view error:`, error)
return NextResponse.json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Failed to load shared content',
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
}🔒 Security & Validation Standards
Rate Limiting Implementation
// /lib/rate-limiter.ts
interface RateLimitResult {
allowed: boolean
limit: number
remaining: number
resetTime: number
retryAfter?: number
message?: string
details?: any
}
export async function checkRateLimit(
request: NextRequest,
action: string
): Promise<RateLimitResult> {
const clientIP = request.ip || request.headers.get('x-forwarded-for') || 'unknown'
const sessionId = request.headers.get('x-session-id')
const userAgent = request.headers.get('user-agent')
// Create composite key for rate limiting
const rateLimitKey = `${action}:${clientIP}:${sessionId}`
// Different limits per action type
const rateLimits = {
'{tool}_generation': { limit: 5, window: 3600 }, // 5 per hour
'{tool}_response': { limit: 10, window: 3600 }, // 10 per hour
'{tool}_export': { limit: 20, window: 3600 } // 20 per hour
}
const config = rateLimits[action] || { limit: 10, window: 3600 }
// Implementation with Redis or Supabase-based tracking
// Returns structured rate limit result
return {
allowed: true, // or false based on current usage
limit: config.limit,
remaining: config.limit - currentUsage,
resetTime: windowResetTime,
message: rateLimitExceeded ? 'Rate limit exceeded' : undefined,
details: { action, currentUsage, windowStart: windowResetTime }
}
}Input Validation Standards
// Universal validation schemas using Zod
import { z } from 'zod'
export const GenerationOptionsSchema = z.object({
// Tool-specific options validation
// Each tool defines its own schema extending this base
})
export const SessionSchema = z.object({
sessionId: z.string().min(1).regex(/^sess_[a-zA-Z0-9_-]+$/),
userAgent: z.string().optional(),
clientIP: z.string().optional()
})
export const ResponseSchema = z.object({
title: z.string().min(1).max(200).trim(),
content: z.string().min(1).max(50000).trim(),
isPublic: z.boolean().optional().default(false)
})
// Usage in API endpoints
export function validateRequest<T>(
data: unknown,
schema: z.ZodSchema<T>
): { success: true; data: T } | { success: false; error: z.ZodError } {
const result = schema.safeParse(data)
if (result.success) {
return { success: true, data: result.data }
} else {
return { success: false, error: result.error }
}
}This comprehensive API design pattern ensures every MyStoryFlow tool delivers consistent, secure, and performant endpoints while maintaining the flexibility needed for tool-specific functionality.