Skip to content

Service Layer Patterns

This document describes the standard patterns for implementing services in the app application. Our service architecture follows a layered approach with clear separation of concerns between base business logic, server-side operations, client-side operations, and WhatsApp bot integrations.

Overview

Services in our application follow a four-layer architecture pattern, with each layer having specific responsibilities:

  1. Base Layer (base.ts) - Pure business logic and database CRUD operations
  2. Server Layer (server.ts) - Server-side operations with external services (AI, geocoding, etc.)
  3. Client Layer (client.ts) - Client-side operations for React components
  4. Sana Layer (sana.ts) - WhatsApp bot-specific integrations

Important: Services must use helper functions (databaseHelper, storageHelper, authHelper) instead of direct Supabase client calls. This provides abstraction and makes the codebase more maintainable.

Service Layer Architecture

Base Service Layer (base.ts)

Purpose: Pure business logic and database operations only. No external service calls.

Responsibilities:

  • CRUD operations using databaseHelper
  • Business logic and validation
  • Data transformations
  • Internal service calculations

Restrictions:

  • ❌ NO external API calls (AI, geocoding, email, etc.)
  • ❌ NO calls to other services' server layers
  • ❌ NO side effects beyond database operations
  • ❌ NO ServiceResponse pattern (return raw types)
  • ❌ NO logging (no clientLogger, serverLogger, or console)
  • ❌ NO error handling with try/catch (let errors bubble up)
  • ✅ CAN call other base services
  • ✅ CAN use helper functions (databaseHelper, storageHelper)
  • ✅ Returns raw data types (T | null, boolean, string, etc.)

Example:

// packages/app/src/services/transaction/base.ts
export async function createTransaction(
  data: TransactionCreateData,
  client?: DatabaseClient
): Promise<Transaction | null> {
  // Pure database operation - no external calls
  // Returns raw type, not ServiceResponse
  const transaction = await create<Transaction>(
    DatabaseTable.TRANSACTIONS,
    data,
    { client }
  );
  return transaction;
}

export async function updateTransactionStatus(
  id: string,
  status: string,
  client?: DatabaseClient
): Promise<Transaction | null> {
  // Business logic + database update only
  // Returns raw type, not ServiceResponse
  const updated = await update<Transaction>(
    DatabaseTable.TRANSACTIONS,
    id,
    { status },
    { client }
  );
  return updated;
}

export async function deleteTransaction(
  id: string,
  client?: DatabaseClient
): Promise<boolean> {
  // Returns boolean for success/failure
  return await deleteRecord(DatabaseTable.TRANSACTIONS, id, { client });
}

Server Service Layer (server.ts)

Purpose: Handle server-side operations that require external services or complex orchestration.

Responsibilities:

  • AI service integrations
  • Geocoding and mapping services
  • Email and notification services
  • Complex multi-service orchestrations
  • Wrapping base operations with external enhancements

Patterns:

  • ✅ CAN call its own base service
  • ✅ CAN call other services' server layers
  • ✅ CAN make external API calls
  • ❌ SHOULD NOT import other services' base layers directly

Example:

// packages/app/src/services/transaction/server.ts
import { createTransaction as createTransactionBase } from './base';
import { createAddress } from '@/services/address/server'; // Server-to-server OK
import { aiHubClient } from '@/services/aiHub/server';
import serverLogger from '@/lib/logger/server';

export async function processOcrExtraction(
  step: AIProcessingJobStep
): Promise<ServiceResponse<Transaction | null>> {
  try {
    const { result_data } = step;

    // Create transaction using base layer (returns raw type)
    const transaction = await createTransactionBase(data);

    if (!transaction) {
      throw new Error('Failed to create transaction');
    }

    if (result_data.address) {
      // Call another service's server layer for AI-enhanced address creation
      const addressResult = await createAddress({
        ...result_data.address,
        transactionId: transaction.id,
      });

      if (addressResult.error) {
        serverLogger.error('Failed to create address', addressResult.error);
      }
    }

    return { data: transaction, error: null };
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    serverLogger.error('Error processing OCR extraction', err);
    return { data: null, error: err };
  }
}

Client Service Layer (client.ts)

Purpose: Client-side operations for React components.

Responsibilities:

  • Data fetching for UI components
  • Client-side state management helpers
  • Browser-specific operations
  • React Query integration helpers

Patterns:

  • Uses client-side Supabase instance
  • Optimized for React component consumption
  • Handles client-side caching strategies

Example:

// packages/app/src/services/transaction/client.ts
import { fetchUserTransactions as fetchUserTransactionsBase } from './base';
import clientLogger from '@/lib/logger/client';

export async function fetchUserTransactions(
  userId: string,
  client?: DatabaseClient
): Promise<ServiceResponse<Transaction[]>> {
  try {
    // Call base layer which returns raw type
    const transactions = await fetchUserTransactionsBase(userId, client);
    return { data: transactions, error: null };
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    clientLogger.error('Error fetching user transactions', err, { userId });
    return { data: null, error: err };
  }
}

Sana Service Layer (sana.ts)

Purpose: WhatsApp bot-specific implementations.

Responsibilities:

  • WhatsApp message handling
  • Bot-specific business logic
  • WhatsApp API integrations
  • Conversation flow management

Patterns:

  • Can use both base and server layers
  • Handles WhatsApp-specific data formats
  • Manages conversation state

Example:

// packages/app/src/services/transaction/sana.ts
import { createTransaction } from './base';
import { processWithAI } from './server';
import sanaLogger from '@/lib/logger/sana';

export async function handleTransactionUpload(
  message: WhatsAppMessage,
  fileUrl: string
): Promise<WhatsAppResponse> {
  try {
    // Create transaction (base returns raw type)
    const transaction = await createTransaction(data);

    if (!transaction) {
      throw new Error('Failed to create transaction');
    }

    // Process with AI (server layer returns ServiceResponse)
    const aiResult = await processWithAI(transaction.id, fileUrl);

    if (aiResult.error) {
      sanaLogger.error('AI processing failed', aiResult.error);
    }

    // Return WhatsApp-formatted response
    return formatWhatsAppResponse(transaction);
  } catch (error) {
    sanaLogger.error('Error handling transaction upload', error as Error);
    return createErrorResponse('Failed to process transaction');
  }
}

Service Communication Patterns

Allowed Communication Flows

Client → Base (same service)
Client → Server (for external operations)
Server → Base (same service)
Server → Server (other services)
Sana → Base (any service)
Sana → Server (any service)
Base → Base (any service)

Prohibited Communication Flows

❌ Server → Other Base (use other's server layer instead)
❌ Base → Server (base should be pure)
❌ Base → External APIs (use server layer)

Example: Address Creation with AI Matching

// ❌ WRONG - Base layer calling external service
// packages/app/src/services/address/base.ts
export async function createAddress(data) {
  const aiMatch = await callAIService(data); // ❌ NO external calls in base
  return create(DatabaseTable.ADDRESSES, data);
}

// ✅ CORRECT - Server layer handles AI, base handles CRUD
// packages/app/src/services/address/server.ts
import { createAddress as createAddressBase } from './base';
import { findExistingAddresses } from './base';
import { aiHubClient } from '@/services/aiHub/server';

export async function createAddress(data) {
  // Use AI to find potential duplicates
  const existingAddresses = await findExistingAddresses();
  const aiMatch = await aiHubClient.findAddressMatch(data, existingAddresses);

  if (aiMatch) {
    return { data: aiMatch, error: null };
  }

  // Geocode the new address
  const geocoded = await geocodeAddress(data);

  // Create using base layer
  return createAddressBase({ ...data, ...geocoded });
}

Location and Naming

  • Location: /packages/app/src/services/[service-name]/
  • File Structure:
    services/
    └── transaction/
        ├── base.ts      # Pure business logic
        ├── server.ts    # Server-side with external services
        ├── client.ts    # Client-side operations
        └── sana.ts      # WhatsApp bot integration
    
  • Import Convention:
    // Import specific layer
    import { createTransaction } from '@/services/transaction/base';
    import { processWithAI } from '@/services/transaction/server';
    

Service Response Pattern

IMPORTANT: The ServiceResponse pattern is ONLY used in wrapper layers (client.ts, server.ts, sana.ts), NOT in base.ts.

Base Layer Returns

Base layer functions return raw data types:

// base.ts - Returns raw types
export async function getUser(id: string): Promise<User | null> {
  return await findOne<User>(DatabaseTable.USERS, { id });
}

export async function deleteUser(id: string): Promise<boolean> {
  return await deleteRecord(DatabaseTable.USERS, id);
}

Wrapper Layers Return ServiceResponse

Client, server, and sana layers wrap base functions with ServiceResponse:

interface ServiceResponse<T> {
  data: T | null;
  error: Error | null;
}

// client.ts - Wraps with ServiceResponse and adds logging
export async function getUser(
  id: string
): Promise<ServiceResponse<User | null>> {
  try {
    const user = await getUserBase(id);
    return { data: user, error: null };
  } catch (error) {
    const err = error as Error;
    clientLogger.error('Failed to get user', err, { id });
    return { data: null, error: err };
  }
}

Error Handling

Base Layer - No Error Handling

Base layer functions let errors bubble up naturally:

// base.ts - No try/catch, no logging
import { findOne } from '@/lib/databaseHelper';
import { DatabaseTable } from '@/constants/database';

export async function fetchSomething(
  id: string,
  client?: DatabaseClient
): Promise<Something | null> {
  // Just return the result directly
  const data = await findOne<Something>(
    DatabaseTable.SOMETHING,
    { id },
    {
      select: 'id, name, status',
      client,
    }
  );

  return data;
}

Wrapper Layers - Handle Errors

Client/server/sana layers add error handling and logging:

// client.ts - Adds error handling and logging
import { fetchSomething as fetchSomethingBase } from './base';
import clientLogger from '@/lib/logger/client';

export async function fetchSomething(
  id: string,
  client?: DatabaseClient
): Promise<ServiceResponse<Something | null>> {
  try {
    const data = await fetchSomethingBase(id, client);

    if (!data) {
      const error = new Error('Something not found');
      clientLogger.error('Failed to fetch something', error, { id });
      return { data: null, error };
    }

    return { data, error: null };
  } catch (error) {
    const err = error as Error;
    clientLogger.error('Unexpected error fetching something', err, { id });
    return { data: null, error: err };
  }
}

Multi-Step Operations

Base Layer - Simple Multi-Step Logic

// base.ts - Multi-step without error handling
import { create } from '@/lib/databaseHelper';
import { uploadFile, deleteFile } from '@/lib/storageHelper';
import { DatabaseTable, StorageBucket } from '@/constants/database';

export async function createWithFile(
  data: CreateData,
  file: File,
  client?: DatabaseClient
): Promise<Result | null> {
  // Step 1: Upload file
  const uploadResult = await uploadFile({
    path: 'some/path/file.jpg',
    file,
    bucketName: StorageBucket.FILES,
    contentType: 'image/jpeg',
    database: client,
  });

  if (!uploadResult) {
    return null;
  }

  // Step 2: Create database record
  const record = await create<Result>(
    DatabaseTable.SOME_TABLE,
    {
      ...data,
      file_path: uploadResult.path,
    },
    { client }
  );

  if (!record) {
    // Cleanup on failure
    await deleteFile({
      path: uploadResult.path,
      bucketName: StorageBucket.FILES,
      database: client,
    });
    return null;
  }

  return record;
}

Wrapper Layer - With Error Handling

// client.ts - Adds error handling and logging
import { createWithFile as createWithFileBase } from './base';
import clientLogger from '@/lib/logger/client';

export async function createWithFile(
  data: CreateData,
  file: File,
  client?: DatabaseClient
): Promise<ServiceResponse<Result>> {
  try {
    const result = await createWithFileBase(data, file, client);

    if (!result) {
      const error = new Error('Failed to create record with file');
      clientLogger.error('Creation failed', error);
      return { data: null, error };
    }

    return { data: result, error: null };
  } catch (error) {
    const err = error as Error;
    clientLogger.error('Unexpected error in createWithFile', err);
    return { data: null, error: err };
  }
}

File Upload Pattern

Base Layer - File Upload Logic

Base layer handles the core upload logic without ServiceResponse:

// base.ts - Pure upload logic
import { create, update } from '@/lib/databaseHelper';
import { uploadFile, deleteFile } from '@/lib/storageHelper';
import { DatabaseTable, StorageBucket } from '@/constants/database';

export async function uploadProjectFile(
  file: File,
  projectId: string,
  userId: string,
  client?: DatabaseClient
): Promise<{ attachmentId: string; storagePath: string } | null> {
  // Generate unique filename
  const timestamp = Date.now();
  const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
  const storedFilename = `${projectId}_${timestamp}_${sanitizedName}`;
  const storagePath = `projects/${projectId}/files/${storedFilename}`;

  // Upload to storage
  const uploadResult = await uploadFile({
    path: storagePath,
    file,
    bucketName: StorageBucket.FILES,
    contentType: file.type,
    upsert: false,
    database: client,
  });

  if (!uploadResult) {
    return null;
  }

  // Create attachment record
  const attachment = await create(
    DatabaseTable.ATTACHMENTS,
    {
      parent_entity_type: 'project',
      parent_entity_id: projectId,
      file_name_original: file.name,
      file_name_stored: storedFilename,
      storage_path_or_url: `${StorageBucket.FILES}/${storagePath}`,
      mime_type: file.type,
      file_size_bytes: file.size,
      uploaded_by_user_id: userId,
    },
    { client, select: 'id' }
  );

  if (!attachment || !('id' in attachment)) {
    // Cleanup on failure
    await deleteFile({
      path: storagePath,
      bucketName: StorageBucket.FILES,
      database: client,
    });
    return null;
  }

  // Update parent record
  const updatedProject = await update(
    DatabaseTable.PROJECTS,
    projectId,
    { attachment_id: attachment.id as string },
    { client }
  );

  if (!updatedProject) {
    // Note: Attachment remains but parent not updated
    // Wrapper layer can decide how to handle this
    return null;
  }

  return {
    attachmentId: attachment.id as string,
    storagePath: `${StorageBucket.FILES}/${storagePath}`,
  };
}

Wrapper Layer - File Upload with Error Handling

// client.ts - Adds authentication, error handling, and logging
import { uploadProjectFile as uploadProjectFileBase } from './base';
import * as authHelper from '@/lib/authHelper';
import clientLogger from '@/lib/logger/client';

export async function uploadProjectFile(
  file: File,
  projectId: string
): Promise<ServiceResponse<{ attachmentId: string; storagePath: string }>> {
  try {
    // Get current user
    const user = await authHelper.getCurrentUser();
    if (!user) {
      const error = new Error('User not authenticated');
      clientLogger.error('Authentication required for upload', error);
      return { data: null, error };
    }

    // Call base layer
    const result = await uploadProjectFileBase(file, projectId, user.id);

    if (!result) {
      const error = new Error('Upload failed');
      clientLogger.error('Failed to upload project file', error, { projectId });
      return { data: null, error };
    }

    clientLogger.info('File uploaded successfully', {
      projectId,
      attachmentId: result.attachmentId,
    });

    return { data: result, error: null };
  } catch (error) {
    const err = error as Error;
    clientLogger.error('Unexpected error uploading file', err, { projectId });
    return { data: null, error: err };
  }
}

Authentication in Services

IMPORTANT: Authentication checks should be done in wrapper layers (client/server/sana), NOT in base.ts.

Base Layer

Base functions receive userId as a parameter when needed:

// base.ts - Receives userId as parameter
export async function createUserProject(
  userId: string,
  projectData: ProjectData,
  client?: DatabaseClient
): Promise<Project | null> {
  return await create<Project>(
    DatabaseTable.PROJECTS,
    {
      ...projectData,
      created_by_user_id: userId,
    },
    { client }
  );
}

Wrapper Layers

Wrapper layers handle authentication and pass userId to base:

// client.ts - Gets current user and passes to base
import * as authHelper from '@/lib/authHelper';
import { createUserProject as createUserProjectBase } from './base';
import clientLogger from '@/lib/logger/client';

export async function createUserProject(
  projectData: ProjectData
): Promise<ServiceResponse<Project>> {
  try {
    // Get current user
    const user = await authHelper.getCurrentUser();
    if (!user) {
      const error = new Error('User not authenticated');
      clientLogger.error('Authentication required', error);
      return { data: null, error };
    }

    // Pass userId to base layer
    const project = await createUserProjectBase(user.id, projectData);

    if (!project) {
      const error = new Error('Failed to create project');
      return { data: null, error };
    }

    clientLogger.info('Project created by user', {
      userId: user.id,
      projectId: project.id,
    });

    return { data: project, error: null };
  } catch (error) {
    const err = error as Error;
    clientLogger.error('Error creating project', err);
    return { data: null, error: err };
  }
}

Testing Services

Mock Setup

// Mock helper dependencies
jest.mock('@/lib/logger/client');
jest.mock('@/lib/databaseHelper');
jest.mock('@/lib/storageHelper');
jest.mock('@/lib/authHelper');

// Import mocked helpers
const { findOne, create, update } = jest.requireActual(
  '@/lib/__mocks__/databaseHelper'
);
const { uploadFile, deleteFile } = jest.requireActual(
  '@/lib/__mocks__/storageHelper'
);
const authHelper = jest.requireActual('@/lib/__mocks__/authHelper');

Test Structure

import { findOne } from '@/lib/databaseHelper';
import { DatabaseTable } from '@/constants/database';
import { serviceMethod } from './serviceFile';

// Mock the helper functions
const mockFindOne = findOne as jest.MockedFunction<typeof findOne>;

describe('serviceName', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('methodName', () => {
    it('should handle success case', async () => {
      // Mock successful database response
      const mockData = { id: '123', name: 'Test' };
      mockFindOne.mockResolvedValue(mockData);

      const result = await serviceMethod('123');

      expect(result.data).toEqual(mockData);
      expect(result.error).toBeNull();
      expect(mockFindOne).toHaveBeenCalledWith(
        DatabaseTable.SOMETHING,
        { id: '123' },
        expect.objectContaining({
          select: expect.any(String),
        })
      );
    });

    it('should handle not found case', async () => {
      // Mock database returning null (not found)
      mockFindOne.mockResolvedValue(null);

      const result = await serviceMethod('nonexistent');

      expect(result.data).toBeNull();
      expect(result.error).toBeInstanceOf(Error);
      expect(result.error?.message).toBe('Something not found');
      expect(clientLogger.error).toHaveBeenCalled();
    });

    it('should handle database error', async () => {
      // Mock database throwing error
      const dbError = new Error('Database connection failed');
      mockFindOne.mockRejectedValue(dbError);

      const result = await serviceMethod('123');

      expect(result.data).toBeNull();
      expect(result.error).toBe(dbError);
      expect(clientLogger.error).toHaveBeenCalledWith(
        'Unexpected error fetching something',
        dbError
      );
    });
  });
});

Type Safety in Tests

Avoid any types:

// ❌ Bad - Using any type
const mockAuthHelper = {
  getCurrentUser: jest.fn().mockResolvedValue(mockUser),
} as any;

// ✅ Good - Proper typing
const mockAuthHelper = {
  getCurrentUser: jest.fn().mockResolvedValue(mockUser),
  isAuthenticated: jest.fn().mockResolvedValue(true),
} as typeof authHelper;

Example Service Implementation

Here's a complete example following all patterns:

Base Layer (base.ts)

import { findOne, findMany, create, update } from '@/lib/databaseHelper';
import { DatabaseTable } from '@/constants/database';
import { ProjectWithRelations, CreateProjectData } from '@/types/project';
import { DatabaseClient } from '@/types/database';

export async function getProject(
  projectId: string,
  client?: DatabaseClient
): Promise<ProjectWithRelations | null> {
  const project = await findOne<ProjectWithRelations>(
    DatabaseTable.PROJECTS,
    { id: projectId },
    {
      select: `
        id,
        project_code,
        title,
        status,
        poster_attachment:poster_attachment_id (
          storage_path_or_url
        )
      `,
      client,
    }
  );

  return project;
}

export async function createProject(
  data: CreateProjectData,
  client?: DatabaseClient
): Promise<ProjectWithRelations | null> {
  const project = await create<ProjectWithRelations>(
    DatabaseTable.PROJECTS,
    data,
    { client }
  );

  return project;
}

export async function getUserProjects(
  userId: string,
  client?: DatabaseClient
): Promise<ProjectWithRelations[]> {
  const projects = await findMany<ProjectWithRelations>(
    DatabaseTable.PROJECTS,
    { created_by_user_id: userId },
    {
      orderBy: { column: 'created_at', ascending: false },
      client,
    }
  );

  return projects || [];
}

Client Layer (client.ts)

import * as projectBase from './base';
import clientLogger from '@/lib/logger/client';
import { ServiceResponse } from '@/types/database';
import { ProjectWithRelations, CreateProjectData } from '@/types/project';

export async function getProject(
  projectId: string
): Promise<ServiceResponse<ProjectWithRelations | null>> {
  try {
    clientLogger.debug('Fetching project', { projectId });

    const project = await projectBase.getProject(projectId);

    if (!project) {
      const error = new Error('Project not found');
      clientLogger.error('Failed to fetch project', error, { projectId });
      return { data: null, error };
    }

    clientLogger.info('Project fetched successfully', {
      projectId: project.id,
      projectCode: project.project_code,
    });

    return { data: project, error: null };
  } catch (error) {
    const err = error as Error;
    clientLogger.error('Unexpected error fetching project', err, { projectId });
    return { data: null, error: err };
  }
}

export async function createProject(
  data: CreateProjectData
): Promise<ServiceResponse<ProjectWithRelations>> {
  try {
    const project = await projectBase.createProject(data);

    if (!project) {
      const error = new Error('Failed to create project');
      clientLogger.error('Project creation failed', error);
      return { data: null, error };
    }

    clientLogger.info('Project created', { projectId: project.id });
    return { data: project, error: null };
  } catch (error) {
    const err = error as Error;
    clientLogger.error('Error creating project', err);
    return { data: null, error: err };
  }
}

Server Layer (server.ts)

import * as projectBase from './base';
import { sendNotification } from '@/services/notification/server';
import serverLogger from '@/lib/logger/server';
import { ServiceResponse } from '@/types/database';
import { CreateProjectData, ProjectWithRelations } from '@/types/project';

export async function createProjectWithNotification(
  data: CreateProjectData,
  notifyUsers: string[]
): Promise<ServiceResponse<ProjectWithRelations>> {
  try {
    // Create project using base layer
    const project = await projectBase.createProject(data);

    if (!project) {
      throw new Error('Failed to create project');
    }

    // Send notifications (external service)
    for (const userId of notifyUsers) {
      const notifyResult = await sendNotification({
        userId,
        type: 'project_created',
        data: { projectId: project.id, projectName: project.title },
      });

      if (notifyResult.error) {
        serverLogger.warn('Failed to notify user', {
          userId,
          error: notifyResult.error,
        });
      }
    }

    serverLogger.info('Project created with notifications', {
      projectId: project.id,
      notifiedUsers: notifyUsers.length,
    });

    return { data: project, error: null };
  } catch (error) {
    const err = error as Error;
    serverLogger.error('Error in createProjectWithNotification', err);
    return { data: null, error: err };
  }
}

Best Practices

  1. Layer Separation - Keep base layer pure, server layer for external calls
  2. Use helper functions - Never use Supabase client directly, always use databaseHelper, storageHelper, authHelper
  3. Always log errors with context using clientLogger.error()
  4. Never throw errors - always return them in the response object
  5. Include relevant context in error logs for debugging
  6. Handle cleanup for multi-step operations
  7. Test all paths - success, errors, and edge cases
  8. Use TypeScript strictly - no any types
  9. Document complex logic with clear comments
  10. Keep services focused - one service per domain area
  11. Import from constants - Use DatabaseTable and StorageBucket enums for consistency
  12. Service boundaries - Respect layer responsibilities and communication patterns
  13. Idempotency - Design operations to be safely retryable
  14. Transaction boundaries - Keep database transactions within base layer

Migration Guide

When refactoring existing code to use current service patterns:

  1. Separate concerns into appropriate layers:
  2. Move pure CRUD operations to base.ts
  3. Move external service calls to server.ts
  4. Keep client-specific logic in client.ts
  5. Extract WhatsApp logic to sana.ts

  6. Replace direct Supabase calls with helper function calls:

  7. supabaseClient.from().select()findOne() or findMany()
  8. supabaseClient.from().insert()create()
  9. supabaseClient.from().update()update()
  10. supabaseClient.storage.from().upload()uploadFile()
  11. supabaseClient.auth.getUser()authHelper.getCurrentUser()

  12. Update service-to-service communication:

  13. Change import from './otherService/base' to import from './otherService/server'
  14. Move external API calls from base to server layer

  15. Update imports to use helper functions and constants

  16. Replace direct error handling with ServiceResponse pattern
  17. Update tests to mock helper functions instead of Supabase client
  18. Add comprehensive tests for each service layer
  19. Update error boundaries if needed

Helper Function Reference

Database Helper Functions

import {
  findOne,
  findMany,
  create,
  update,
  remove,
  ServiceResponse,
} from '@/lib/databaseHelper';
import { DatabaseTable } from '@/constants/database';

// Find single record
const user = await findOne<User>(DatabaseTable.USERS, { id: 'user-id' });

// Find multiple records with conditions
const projects = await findMany<Project>(
  DatabaseTable.PROJECTS,
  { status: 'active' },
  { orderBy: { column: 'created_at', ascending: false } }
);

// Create new record
const newProject = await create<Project>(DatabaseTable.PROJECTS, {
  title: 'New Project',
  status: 'draft',
});

// Update existing record
const updatedProject = await update(DatabaseTable.PROJECTS, 'project-id', {
  status: 'active',
});

Storage Helper Functions

import { uploadFile, deleteFile, getStorageUrl } from '@/lib/storageHelper';
import { StorageBucket } from '@/constants/database';

// Upload file
const uploadResult = await uploadFile({
  path: 'projects/poster.jpg',
  file: fileObject,
  bucketName: StorageBucket.FILES,
  contentType: 'image/jpeg',
});

// Delete file
const deleteSuccess = await deleteFile({
  path: 'projects/poster.jpg',
  bucketName: StorageBucket.FILES,
});

// Get file URL
const fileUrl = await getStorageUrl({
  path: 'projects/poster.jpg',
  bucketName: StorageBucket.FILES,
});

Auth Helper Functions

import * as authHelper from '@/lib/authHelper';

// Get current user
const user = await authHelper.getCurrentUser();

// Check authentication status
const isAuth = await authHelper.isAuthenticated();

Future Considerations

  • Consider adding request caching for frequently accessed data
  • Implement retry logic for transient failures in helper functions
  • Add request debouncing for user-triggered operations
  • Consider service composition for complex workflows
  • Enhance helper functions with more advanced query capabilities