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.
Helper Architecture Overview
The architecture separates helpers into three distinct layers based on execution context and responsibilities:Three-Layer Helper System:Key Principles:
🔵 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
- Execution Context Isolation - Each layer runs in its intended environment
- Dependency Direction - Higher layers can use lower layers, not vice versa
- Resource Access Control - Access restricted by layer boundaries
- Type Safety - Proper TypeScript boundaries between layers
Client Layer Helpers
Client helpers are pure functions that run exclusively in the browser:Pure Client Functions:Client Validation Helpers:
// 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) + '...'
}// 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 Layer Helpers
Service helpers contain server-side business logic and can access server resources:Server Business Logic:Email and Notification Helpers:
// 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
}// 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 Layer Helpers
Database helpers focus on query optimization, data transformation, and ORM utilities:Common Query Patterns:Advanced Query Utilities:
// 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
}
}// ✅ 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)
}Migration Strategy
How to migrate existing helpers to the proper architecture:Step-by-Step Migration:1. Analyze Current Helpers:2. Categorize by Usage:3. Split the Helper:4. Update Import Statements:
# 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/// 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
}// ✅ 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)
}// 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'Best Practices Summary
🔵
Client Helper Rules
• Pure functions only - No side effects
• Browser-compatible - No Node.js APIs
• Parameter-based - No environment access
• Testable - Easy to unit test
• Reusable - Can be called from server
🟠
Service Helper Rules
• Server resources OK - Environment, APIs, files
• Business logic - Complex operations
• Can use client helpers - Pass data as parameters
• External integrations - Third-party APIs
• Validation - Server-side rules
🟢
Database Helper Rules
• Query optimization - Efficient database access
• Data transformation - Type conversions
• Migration utilities - Schema evolution
• Batch operations - Performance optimization
• No business logic - Data operations only
Helper architecture ready! Your utilities are properly separated by execution context, preventing runtime errors and maintaining clean boundaries between client and server code.