Helpers Architecture

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.

Helper Architecture Overview

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:
  • 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:
// 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 Layer Helpers

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 Layer Helpers

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)
}

Migration Strategy

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'

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.
    Helpers Architecture | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days