Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Technical DocumentationTools InfrastructureAPI Design Patterns & Standards

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.