Files
flyer-crawler.projectium.com/docs/development/CODE-PATTERNS.md
Torben Sorensen 0d4b028a66
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 21m49s
design fixup and docs + api versioning
2026-01-28 00:04:56 -08:00

11 KiB

Code Patterns

Common code patterns extracted from Architecture Decision Records (ADRs). Use these as templates when writing new code.

Table of Contents


Error Handling

ADR: ADR-001

Repository Layer Error Handling

import { handleDbError, NotFoundError } from '../services/db/errors.db';
import { PoolClient } from 'pg';

export async function getFlyerById(id: number, client?: PoolClient): Promise<Flyer> {
  const db = client || pool;

  try {
    const result = await db.query('SELECT * FROM flyers WHERE id = $1', [id]);

    if (result.rows.length === 0) {
      throw new NotFoundError('Flyer', id);
    }

    return result.rows[0];
  } catch (error) {
    throw handleDbError(error);
  }
}

Route Layer Error Handling

import { sendError } from '../utils/apiResponse';

app.get('/api/v1/flyers/:id', async (req, res) => {
  try {
    const flyer = await flyerDb.getFlyerById(parseInt(req.params.id));
    return sendSuccess(res, flyer);
  } catch (error) {
    // IMPORTANT: Use req.originalUrl for dynamic path logging (not hardcoded paths)
    req.log.error({ error }, `Error in ${req.originalUrl.split('?')[0]}:`);
    return sendError(res, error);
  }
});

Best Practice: Always use req.originalUrl.split('?')[0] in error log messages instead of hardcoded paths. This ensures logs reflect the actual request URL including version prefixes (/api/v1/). See Error Logging Path Patterns for details.

Custom Error Types

// NotFoundError - Entity not found
throw new NotFoundError('Flyer', id);

// ValidationError - Invalid input
throw new ValidationError('Invalid email format');

// DatabaseError - Database operation failed
throw new DatabaseError('Failed to insert flyer', originalError);

Repository Patterns

ADR: ADR-034

Method Naming Conventions

Prefix Returns Not Found Behavior Use Case
get* Entity Throws NotFoundError When entity must exist
find* Entity | null Returns null When entity may not exist
list* Array Returns [] When returning multiple

Get Method (Must Exist)

/**
 * Get a flyer by ID. Throws NotFoundError if not found.
 */
export async function getFlyerById(id: number, client?: PoolClient): Promise<Flyer> {
  const db = client || pool;

  try {
    const result = await db.query('SELECT * FROM flyers WHERE id = $1', [id]);

    if (result.rows.length === 0) {
      throw new NotFoundError('Flyer', id);
    }

    return result.rows[0];
  } catch (error) {
    throw handleDbError(error);
  }
}

Find Method (May Not Exist)

/**
 * Find a flyer by ID. Returns null if not found.
 */
export async function findFlyerById(id: number, client?: PoolClient): Promise<Flyer | null> {
  const db = client || pool;

  try {
    const result = await db.query('SELECT * FROM flyers WHERE id = $1', [id]);

    return result.rows[0] || null;
  } catch (error) {
    throw handleDbError(error);
  }
}

List Method (Multiple Results)

/**
 * List all active flyers. Returns empty array if none found.
 */
export async function listActiveFlyers(client?: PoolClient): Promise<Flyer[]> {
  const db = client || pool;

  try {
    const result = await db.query(
      'SELECT * FROM flyers WHERE end_date >= CURRENT_DATE ORDER BY start_date DESC',
    );

    return result.rows;
  } catch (error) {
    throw handleDbError(error);
  }
}

API Response Patterns

ADR: ADR-028

Success Response

import { sendSuccess } from '../utils/apiResponse';

app.post('/api/flyers', async (req, res) => {
  const flyer = await flyerService.createFlyer(req.body);
  return sendSuccess(res, flyer, 'Flyer created successfully', 201);
});

Paginated Response

import { sendPaginated } from '../utils/apiResponse';

app.get('/api/flyers', async (req, res) => {
  const { page = 1, pageSize = 20 } = req.query;
  const { items, total } = await flyerService.listFlyers(page, pageSize);

  return sendPaginated(res, {
    items,
    total,
    page: parseInt(page),
    pageSize: parseInt(pageSize),
  });
});

Error Response

import { sendError } from '../utils/apiResponse';

app.get('/api/flyers/:id', async (req, res) => {
  try {
    const flyer = await flyerDb.getFlyerById(parseInt(req.params.id));
    return sendSuccess(res, flyer);
  } catch (error) {
    return sendError(res, error); // Automatically maps error to correct status
  }
});

Transaction Management

ADR: ADR-002

Basic Transaction

import { withTransaction } from '../services/db/transaction.db';

export async function createFlyerWithItems(
  flyerData: FlyerInput,
  items: FlyerItemInput[],
): Promise<Flyer> {
  return withTransaction(async (client) => {
    // Create flyer
    const flyer = await flyerDb.createFlyer(flyerData, client);

    // Create items
    const createdItems = await flyerItemDb.createItems(
      items.map((item) => ({ ...item, flyer_id: flyer.id })),
      client,
    );

    // Automatically commits on success, rolls back on error
    return { ...flyer, items: createdItems };
  });
}

Nested Transactions

export async function bulkImportFlyers(flyersData: FlyerInput[]): Promise<ImportResult> {
  return withTransaction(async (client) => {
    const results = [];

    for (const flyerData of flyersData) {
      try {
        // Each flyer import is atomic
        const flyer = await createFlyerWithItems(
          flyerData,
          flyerData.items,
          client, // Pass transaction client
        );
        results.push({ success: true, flyer });
      } catch (error) {
        results.push({ success: false, error: error.message });
      }
    }

    return results;
  });
}

Input Validation

ADR: ADR-003

Zod Schema Definition

// src/schemas/flyer.schemas.ts
import { z } from 'zod';

export const createFlyerSchema = z.object({
  store_id: z.number().int().positive(),
  image_url: z
    .string()
    .url()
    .regex(/^https?:\/\/.*/),
  start_date: z.string().datetime(),
  end_date: z.string().datetime(),
  items: z
    .array(
      z.object({
        name: z.string().min(1).max(255),
        price: z.number().positive(),
        quantity: z.string().optional(),
      }),
    )
    .min(1),
});

export type CreateFlyerInput = z.infer<typeof createFlyerSchema>;

Route Validation Middleware

import { validateRequest } from '../middleware/validation';
import { createFlyerSchema } from '../schemas/flyer.schemas';

app.post('/api/flyers', validateRequest(createFlyerSchema), async (req, res) => {
  // req.body is now type-safe and validated
  const flyer = await flyerService.createFlyer(req.body);
  return sendSuccess(res, flyer, 'Flyer created successfully', 201);
});

Manual Validation

import { createFlyerSchema } from '../schemas/flyer.schemas';

export async function processFlyer(data: unknown): Promise<Flyer> {
  // Validate and parse input
  const validated = createFlyerSchema.parse(data);

  // Type-safe from here on
  return flyerDb.createFlyer(validated);
}

Authentication

ADR: ADR-048

Protected Route with JWT

import { authenticateJWT } from '../middleware/auth';

app.get(
  '/api/profile',
  authenticateJWT, // Middleware adds req.user
  async (req, res) => {
    // req.user is guaranteed to exist
    const user = await userDb.getUserById(req.user.id);
    return sendSuccess(res, user);
  },
);

Optional Authentication

import { optionalAuth } from '../middleware/auth';

app.get(
  '/api/flyers',
  optionalAuth, // req.user may or may not exist
  async (req, res) => {
    const flyers = req.user
      ? await flyerDb.listFlyersForUser(req.user.id)
      : await flyerDb.listPublicFlyers();

    return sendSuccess(res, flyers);
  },
);

Generate JWT Token

import jwt from 'jsonwebtoken';
import { env } from '../config/env';

export function generateToken(user: User): string {
  return jwt.sign({ id: user.id, email: user.email }, env.JWT_SECRET, { expiresIn: '7d' });
}

Caching

ADR: ADR-029

Cache Pattern

import { cacheService } from '../services/cache.server';

export async function getFlyer(id: number): Promise<Flyer> {
  // Try cache first
  const cached = await cacheService.get<Flyer>(`flyer:${id}`);
  if (cached) return cached;

  // Cache miss - fetch from database
  const flyer = await flyerDb.getFlyerById(id);

  // Store in cache (1 hour TTL)
  await cacheService.set(`flyer:${id}`, flyer, 3600);

  return flyer;
}

Cache Invalidation

export async function updateFlyer(id: number, data: UpdateFlyerInput): Promise<Flyer> {
  const flyer = await flyerDb.updateFlyer(id, data);

  // Invalidate cache
  await cacheService.delete(`flyer:${id}`);
  await cacheService.invalidatePattern('flyers:list:*');

  return flyer;
}

Background Jobs

ADR: ADR-036

Queue Job

import { flyerProcessingQueue } from '../services/queues.server';

export async function enqueueFlyerProcessing(flyerId: number): Promise<void> {
  await flyerProcessingQueue.add(
    'process-flyer',
    {
      flyerId,
      timestamp: Date.now(),
    },
    {
      attempts: 3,
      backoff: {
        type: 'exponential',
        delay: 2000,
      },
    },
  );
}

Process Job

// src/services/workers.server.ts
import { Worker } from 'bullmq';

const flyerWorker = new Worker(
  'flyer-processing',
  async (job) => {
    const { flyerId } = job.data;

    try {
      // Process flyer
      const result = await aiService.extractFlyerData(flyerId);
      await flyerDb.updateFlyerWithData(flyerId, result);

      // Update progress
      await job.updateProgress(100);

      return { success: true, itemCount: result.items.length };
    } catch (error) {
      logger.error('Flyer processing failed', { flyerId, error });
      throw error; // Will retry automatically
    }
  },
  {
    connection: redisConnection,
    concurrency: 5,
  },
);