ignitionstack.pro uses Next.js App Router with Server Actions and API Routes for backend functionality.
Server Actions are the primary way to handle data mutations. They provide:
// server/actions/create-post.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { ActionResult } from '@/lib/action-result'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData): Promise<ActionResult<Post>> {
const supabase = await createClient()
const { data, error } = await supabase
.from('posts')
.insert({
title: formData.get('title') as string,
content: formData.get('content') as string,
})
.select()
.single()
if (error) {
return { success: false, error: error.message }
}
revalidatePath('/blog')
return { success: true, data }
}Server-side data fetching with caching:
// server/queries/get-posts.ts
import { createClient } from '@/lib/supabase/server'
import { cache } from 'react'
import { unstable_cache } from 'next/cache'
export const getPosts = cache(async () => {
const supabase = await createClient()
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('published', true)
.order('created_at', { ascending: false })
if (error) throw error
return data
})
// With cache tag for revalidation
export const getPostsCached = unstable_cache(
async () => getPosts(),
['posts'],
{ revalidate: 60, tags: ['posts'] }
)API Routes are used for:
| Endpoint | Method | Description |
|---|---|---|
/api/stripe/webhook | POST | Handle Stripe events |
/api/stripe/create-checkout | POST | Create checkout session |
// api/stripe/webhook/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { stripe } from '@/lib/stripe'
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
try {
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET_KEY!
)
switch (event.type) {
case 'checkout.session.completed':
// Handle successful payment
break
case 'customer.subscription.updated':
// Handle subscription change
break
}
return NextResponse.json({ received: true })
} catch (error) {
return NextResponse.json(
{ error: 'Webhook error' },
{ status: 400 }
)
}
}| Endpoint | Method | Description |
|---|---|---|
/api/ai/chat | POST | Stream chat response |
/api/ai/upload | POST | Upload documents for RAG |
/api/ai/share | POST | Share conversation |
// api/ai/chat/route.ts
import { NextResponse } from 'next/server'
import { streamText } from '@/lib/ai'
export async function POST(request: Request) {
const { messages, provider, model } = await request.json()
const stream = await streamText({
messages,
provider,
model,
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}| Endpoint | Method | Description |
|---|---|---|
/api/resend/webhook | POST | Handle email events |
All server actions return a standardized result:
// lib/action-result.ts
export type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string }
// Usage
const result = await createPost(formData)
if (result.success) {
console.log('Created:', result.data)
} else {
console.error('Error:', result.error)
}Protect admin-only actions:
// lib/admin-action-wrapper.ts
import { isAdmin } from '@/lib/admin-auth'
export function withAdminAuth<T extends (...args: any[]) => Promise<ActionResult<any>>>(
action: T
): T {
return (async (...args: Parameters<T>) => {
if (!(await isAdmin())) {
return { success: false, error: 'Unauthorized' }
}
return action(...args)
}) as T
}
// Usage
export const deleteUser = withAdminAuth(async (userId: string) => {
// Only admins can delete users
})Data access is abstracted through repositories:
// lib/repositories/user-repository.ts
import { createClient, createAdminClient } from '@/lib/supabase/server'
export const userRepository = {
async findById(id: string) {
const supabase = await createClient()
const { data, error } = await supabase
.from('users')
.select('*')
.eq('id', id)
.single()
if (error) return null
return data
},
async create(input: CreateUserInput) {
const supabase = createAdminClient() // Bypass RLS
const { data, error } = await supabase
.from('users')
.insert(input)
.select()
.single()
if (error) throw error
return data
},
}src/app/server/
├── ai/ # AI-related queries
│ ├── conversations.ts
│ └── messages.ts
├── blog/ # Blog queries
│ ├── posts.ts
│ └── categories.ts
├── products/ # E-commerce
│ └── products.ts
├── projects/ # Project management
│ └── projects.ts
├── users/ # User management
│ └── users.ts
└── insights/ # Analytics
└── insights.tsAPI routes include rate limiting:
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
// Usage in API route
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
const { success } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
// Continue...
}Rate limiting is applied to sensitive endpoints like authentication, payments, and AI chat to prevent abuse.
// lib/supabase/error-handler.ts
import { PostgrestError } from '@supabase/supabase-js'
export function handleSupabaseError(error: PostgrestError): string {
switch (error.code) {
case '23505':
return 'This record already exists'
case '23503':
return 'Referenced record not found'
case '42501':
return 'Permission denied'
default:
return error.message
}
}Structured logging with Pino:
import { logger } from '@/lib/logger'
// In server actions
logger.info({ userId, action: 'create_post' }, 'User created post')
logger.error({ error, userId }, 'Failed to create post')Server Actions provide better security and DX than API routes for form submissions.
Use cache() and unstable_cache for data fetching to reduce database load.
Always validate user input with Zod schemas:
import { z } from 'zod'
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
})Return ActionResult with meaningful error messages instead of throwing.
Abstract database operations for cleaner code and easier testing.