The boilerplate enforces strict separation between client and server helpers based on file location. This architectural pattern ensures proper execution context, prevents runtime errors, and maintains clean boundaries between browser and server code.
Critical Rule: Helper location determines execution context. Client helpers (/lib/helper/) cannot access server resources, while server helpers (/services/helpers/, /db/helpers/) cannot run in the browser.
Perfect for: Maintaining clear client/server boundaries, preventing execution context errors, and organizing utility functions with proper separation of concerns.
The architecture separates helpers into three distinct layers based on execution context and responsibilities:
Three-Layer Helper System:
🔵 CLIENT LAYER (/lib/helper/)
├── Pure client-side functions
├── Browser-compatible utilities
├── UI formatting and validation
└── No server resource access
⬇️
🟠 SERVICE LAYER (/services/helpers/)
├── Server-side business logic
├── Environment variable access
├── External API integrations
└── Complex server operations
⬇️
🟢 DATABASE LAYER (/db/helpers/)
├── Database query utilities
├── Data transformation helpers
├── ORM-specific operations
└── Migration assistance
Key Principles:
Client helpers are pure functions that run exclusively in the browser:
Pure Client Functions:
// src/lib/helper/format-helper.ts
// ✅ Format utilities (no external dependencies)
export function formatPrice(amount: number, currency = 'EUR'): string {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency
}).format(amount)
}
export function formatDate(
date: Date | string,
locale = 'en-US',
options: Intl.DateTimeFormatOptions = {}
): string {
const dateObj = typeof date === 'string' ? new Date(date) : date
return dateObj.toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
...options
})
}
export function formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
if (bytes === 0) return '0 Bytes'
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
}
// ✅ Text utilities
export function slugify(text: string): string {
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w-]+/g, '')
.replace(/--+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
}
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text
return text.substring(0, maxLength - 3) + '...'
}Client Validation Helpers:
// src/lib/helper/validation-helper.ts
export function validateEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(email)
}
export function validatePassword(password: string): {
isValid: boolean
errors: string[]
} {
const errors: string[] = []
if (password.length < 8) {
errors.push('Password must be at least 8 characters')
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain uppercase letter')
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain lowercase letter')
}
if (!/\d/.test(password)) {
errors.push('Password must contain a number')
}
return {
isValid: errors.length === 0,
errors
}
}
export function validateUrl(url: string): boolean {
try {
new URL(url)
return true
} catch {
return false
}
}Service helpers contain server-side business logic and can access server resources:
Server Business Logic:
// src/services/helpers/billing-helper.ts
import { env } from '@/env'
import { getReferenceIdByBillingMode } from '@/lib/helper/subscription-helper'
import type { BillingModes } from '@/services/types/common'
// ✅ Server helper using client helper + server resources
export function getCurrentBillingMode(): BillingModes {
return env.NEXT_PUBLIC_BILLING_MODE as BillingModes
}
export function getCurrentReferenceId(
userId: string,
organizationId?: string
): string {
const billingMode = getCurrentBillingMode()
return getReferenceIdByBillingMode(billingMode, userId, organizationId)
}
// ✅ Complex server logic
export async function calculateSubscriptionMetrics(
subscriptions: Subscription[]
): Promise<{
mrr: number
arr: number
churnRate: number
ltv: number
}> {
const activeSubscriptions = subscriptions.filter(s => s.status === 'active')
// Monthly Recurring Revenue
const mrr = activeSubscriptions.reduce((sum, sub) => {
const monthlyAmount = sub.interval === 'year'
? sub.amount / 12
: sub.amount
return sum + monthlyAmount
}, 0)
// Annual Recurring Revenue
const arr = mrr * 12
// Churn Rate calculation (requires historical data)
const churnRate = await calculateChurnRate(subscriptions)
// Lifetime Value
const averageMonthlyRevenue = mrr / activeSubscriptions.length
const ltv = averageMonthlyRevenue / (churnRate / 100)
return { mrr, arr, churnRate, ltv }
}
async function calculateChurnRate(subscriptions: Subscription[]): Promise<number> {
// Implementation with database queries for historical data
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
// ... complex churn calculation
return 5.2 // Example churn rate percentage
}Email and Notification Helpers:
// src/services/helpers/email-helper.ts
import { env } from '@/env'
import { Resend } from 'resend'
const resend = new Resend(env.RESEND_API_KEY)
export interface EmailTemplate {
to: string
subject: string
template: string
data: Record<string, any>
}
export async function sendTemplatedEmail(
emailData: EmailTemplate
): Promise<{ success: boolean; messageId?: string; error?: string }> {
try {
const result = await resend.emails.send({
from: env.EMAIL_FROM_ADDRESS,
to: emailData.to,
subject: emailData.subject,
html: await renderTemplate(emailData.template, emailData.data),
})
return {
success: true,
messageId: result.data?.id,
}
} catch (error) {
console.error('Email send failed:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
export async function sendWelcomeEmail(
userEmail: string,
userName: string
): Promise<void> {
await sendTemplatedEmail({
to: userEmail,
subject: 'Welcome to Our Platform!',
template: 'welcome',
data: { userName }
})
}
export async function sendSubscriptionConfirmation(
userEmail: string,
subscription: Subscription
): Promise<void> {
await sendTemplatedEmail({
to: userEmail,
subject: 'Subscription Confirmed',
template: 'subscription-confirmation',
data: { subscription }
})
}
async function renderTemplate(
templateName: string,
data: Record<string, any>
): Promise<string> {
// Template rendering logic
const template = await loadEmailTemplate(templateName)
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match)
}Database helpers focus on query optimization, data transformation, and ORM utilities:
Common Query Patterns:
// src/db/helpers/query-helper.ts
import { sql, and, or, eq, gte, lte, desc, asc } from 'drizzle-orm'
import db from '../models/db'
// ✅ Reusable query builders
export function buildDateRangeFilter(
column: any,
startDate?: Date,
endDate?: Date
) {
const conditions = []
if (startDate) {
conditions.push(gte(column, startDate))
}
if (endDate) {
conditions.push(lte(column, endDate))
}
return conditions.length > 0 ? and(...conditions) : undefined
}
export function buildSearchFilter(
columns: any[],
searchTerm?: string
) {
if (!searchTerm) return undefined
const likePattern = `%${searchTerm.toLowerCase()}%`
const conditions = columns.map(column =>
sql`LOWER(${column}) LIKE ${likePattern}`
)
return or(...conditions)
}
export function buildSortOrder(
sortField?: string,
sortDirection: 'asc' | 'desc' = 'desc'
) {
if (!sortField) return undefined
return sortDirection === 'asc' ? asc(sortField) : desc(sortField)
}
// ✅ Generic pagination helper
export interface QueryOptions {
limit?: number
offset?: number
sortField?: string
sortDirection?: 'asc' | 'desc'
searchTerm?: string
dateRange?: {
startDate?: Date
endDate?: Date
column?: any
}
}
export async function executePagedQuery<T>(
baseQuery: any,
countQuery: any,
options: QueryOptions = {}
): Promise<{
data: T[]
totalCount: number
hasMore: boolean
}> {
const limit = Math.min(options.limit || 20, 100)
const offset = options.offset || 0
// Build filters
const conditions = []
if (options.searchTerm && options.searchTerm.length > 0) {
// Search conditions would be added here based on specific table
}
if (options.dateRange?.startDate || options.dateRange?.endDate) {
const dateFilter = buildDateRangeFilter(
options.dateRange.column,
options.dateRange.startDate,
options.dateRange.endDate
)
if (dateFilter) conditions.push(dateFilter)
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined
// Execute queries in parallel
const [data, countResult] = await Promise.all([
baseQuery
.where(whereClause)
.limit(limit)
.offset(offset)
.execute(),
countQuery
.where(whereClause)
.execute()
])
const totalCount = countResult[0]?.count || 0
return {
data,
totalCount,
hasMore: offset + data.length < totalCount
}
}Advanced Query Utilities:
// ✅ Bulk operations
export async function bulkInsert<T>(
table: any,
records: T[],
chunkSize: number = 1000
): Promise<void> {
for (let i = 0; i < records.length; i += chunkSize) {
const chunk = records.slice(i, i + chunkSize)
await db.insert(table).values(chunk)
}
}
export async function bulkUpdate<T>(
table: any,
updates: Array<{ id: string; data: Partial<T> }>,
chunkSize: number = 1000
): Promise<void> {
for (let i = 0; i < updates.length; i += chunkSize) {
const chunk = updates.slice(i, i + chunkSize)
await db.transaction(async (tx) => {
for (const update of chunk) {
await tx
.update(table)
.set(update.data)
.where(eq(table.id, update.id))
}
})
}
}
// ✅ Aggregation helpers
export async function getTableStats(
table: any,
groupByColumn?: any
): Promise<Array<{
group?: string
count: number
createdThisMonth: number
createdThisWeek: number
}>> {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const startOfWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
return await db
.select({
group: groupByColumn,
count: sql<number>`COUNT(*)`,
createdThisMonth: sql<number>`COUNT(CASE WHEN created_at >= ${startOfMonth} THEN 1 END)`,
createdThisWeek: sql<number>`COUNT(CASE WHEN created_at >= ${startOfWeek} THEN 1 END)`,
})
.from(table)
.groupBy(groupByColumn)
}How to migrate existing helpers to the proper architecture:
Step-by-Step Migration:
1. Analyze Current Helpers:
# Find all helper files
find src -name "*helper*" -type f
# Analyze dependencies
grep -r "import.*env" src/lib/helper/
grep -r "process.env" src/lib/helper/
grep -r "import.*db" src/lib/helper/2. Categorize by Usage:
// Before Migration Analysis
// src/lib/helper/subscription-helper.ts - PROBLEMATIC
import { env } from '@/env' // ❌ Server dependency in client helper
export const BILLING_MODE = env.NEXT_PUBLIC_BILLING_MODE // ❌ Server access
export function getReferenceIdByBillingMode(
userId?: string,
organizationId?: string
): string | undefined {
// ✅ This logic is actually pure client logic
return BILLING_MODE === BillingModes.ORGANIZATION
? organizationId || userId
: userId
}3. Split the Helper:
// ✅ After Migration - Client Helper
// src/lib/helper/subscription-helper.ts
export function getReferenceIdByBillingMode(
billingMode: BillingModes, // Now passed as parameter
userId: string,
organizationId?: string
): string {
return billingMode === BillingModes.ORGANIZATION
? organizationId || userId
: userId
}
// ✅ After Migration - Service Helper
// src/services/helpers/billing-helper.ts
import { env } from '@/env'
import { getReferenceIdByBillingMode } from '@/lib/helper/subscription-helper'
export function getCurrentBillingMode(): BillingModes {
return env.NEXT_PUBLIC_BILLING_MODE as BillingModes
}
export function getCurrentReferenceId(
userId: string,
organizationId?: string
): string {
const billingMode = getCurrentBillingMode()
return getReferenceIdByBillingMode(billingMode, userId, organizationId)
}4. Update Import Statements:
// Before: All imports from problematic helper
import { getReferenceIdByBillingMode, BILLING_MODE } from '@/lib/helper/subscription-helper'
// After: Specific imports from appropriate layers
// In client components:
import { getReferenceIdByBillingMode } from '@/lib/helper/subscription-helper'
// In server code:
import { getCurrentReferenceId } from '@/services/helpers/billing-helper'Helper architecture ready! Your utilities are properly separated by execution context, preventing runtime errors and maintaining clean boundaries between client and server code.