# Code Patterns Common code patterns extracted from Architecture Decision Records (ADRs). Use these as templates when writing new code. ## Quick Reference | Pattern | Key Function/Class | Import From | | -------------------- | ------------------------------------------------- | ------------------------------------- | | **tsoa Controllers** | `BaseController`, `@Route`, `@Security` | `src/controllers/base.controller.ts` | | Error Handling | `handleDbError()`, `NotFoundError` | `src/services/db/errors.db.ts` | | Repository Methods | `get*`, `find*`, `list*` | `src/services/db/*.db.ts` | | API Responses | `sendSuccess()`, `sendPaginated()`, `sendError()` | `src/utils/apiResponse.ts` | | Transactions | `withTransaction()` | `src/services/db/connection.db.ts` | | Validation | `validateRequest()` | `src/middleware/validation.ts` | | Authentication | `authenticateJWT`, `@Security('bearerAuth')` | `src/middleware/auth.ts` | | Caching | `cacheService` | `src/services/cache.server.ts` | | Background Jobs | Queue classes | `src/services/queues.server.ts` | | Feature Flags | `isFeatureEnabled()`, `useFeatureFlag()` | `src/services/featureFlags.server.ts` | --- ## Table of Contents - [tsoa Controllers](#tsoa-controllers) - [Error Handling](#error-handling) - [Repository Patterns](#repository-patterns) - [API Response Patterns](#api-response-patterns) - [Transaction Management](#transaction-management) - [Input Validation](#input-validation) - [Authentication](#authentication) - [Caching](#caching) - [Background Jobs](#background-jobs) - [Feature Flags](#feature-flags) --- ## tsoa Controllers **ADR**: [ADR-018](../adr/0018-api-documentation-strategy.md), [ADR-059](../adr/0059-dependency-modernization.md) All API endpoints are implemented as tsoa controller classes that extend `BaseController`. This pattern provides type-safe OpenAPI documentation generation and standardized response formatting. ### Basic Controller Structure ```typescript import { Route, Tags, Get, Post, Body, Path, Query, Security, SuccessResponse, Response, } from 'tsoa'; import type { Request as ExpressRequest } from 'express'; import { BaseController, SuccessResponse as SuccessResponseType, ErrorResponse, } from './base.controller'; interface CreateItemRequest { name: string; description?: string; } interface ItemResponse { id: number; name: string; created_at: string; } @Route('items') @Tags('Items') export class ItemController extends BaseController { /** * Get an item by ID. * @summary Get item * @param id Item ID */ @Get('{id}') @SuccessResponse(200, 'Item retrieved') @Response(404, 'Item not found') public async getItem(@Path() id: number): Promise> { const item = await itemService.getItemById(id); return this.success(item); } /** * Create a new item. Requires authentication. * @summary Create item */ @Post() @Security('bearerAuth') @SuccessResponse(201, 'Item created') @Response(401, 'Not authenticated') public async createItem( @Body() body: CreateItemRequest, @Request() request: ExpressRequest, ): Promise> { const user = request.user as UserProfile; const item = await itemService.createItem(body, user.user.user_id); return this.created(item); } } ``` ### BaseController Response Helpers ```typescript // Success response (200) return this.success(data); // Created response (201) return this.created(data); // Paginated response const { page, limit } = this.normalizePagination(queryPage, queryLimit); return this.paginated(items, { page, limit, total }); // Message-only response return this.message('Operation completed'); // No content (204) return this.noContent(); ``` ### Authentication with @Security ```typescript import { Security, Request } from 'tsoa'; import { requireAdminRole } from '../middleware/tsoaAuthentication'; // Require authentication @Get('profile') @Security('bearerAuth') public async getProfile(@Request() req: ExpressRequest): Promise<...> { const user = req.user as UserProfile; return this.success(user); } // Require admin role @Delete('users/{id}') @Security('bearerAuth') public async deleteUser(@Path() id: string, @Request() req: ExpressRequest): Promise { requireAdminRole(req.user as UserProfile); await userService.deleteUser(id); return this.noContent(); } ``` ### Error Handling in Controllers ```typescript import { NotFoundError, ValidationError, ForbiddenError } from './base.controller'; // Throw errors - they're handled by the global error handler throw new NotFoundError('Item', id); // 404 throw new ValidationError([], 'Invalid'); // 400 throw new ForbiddenError('Admin only'); // 403 ``` ### Rate Limiting ```typescript import { Middlewares } from 'tsoa'; import { loginLimiter } from '../config/rateLimiters'; @Post('login') @Middlewares(loginLimiter) @Response(429, 'Too many attempts') public async login(@Body() body: LoginRequest): Promise<...> { ... } ``` ### Regenerating Routes After modifying controllers, regenerate the tsoa routes: ```bash npm run tsoa:spec && npm run tsoa:routes ``` **Full Guide**: See [TSOA-MIGRATION-GUIDE.md](./TSOA-MIGRATION-GUIDE.md) for comprehensive documentation. --- ## Error Handling **ADR**: [ADR-001](../adr/0001-standardized-error-handling.md) ### Repository Layer Error Handling ```typescript import { handleDbError, NotFoundError } from '../services/db/errors.db'; import { PoolClient } from 'pg'; export async function getFlyerById(id: number, client?: PoolClient): Promise { 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 ```typescript 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](ERROR-LOGGING-PATHS.md) for details. ### Custom Error Types ```typescript // 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](../adr/0034-repository-pattern-standards.md) ### 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) ```typescript /** * Get a flyer by ID. Throws NotFoundError if not found. */ export async function getFlyerById(id: number, client?: PoolClient): Promise { 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) ```typescript /** * Find a flyer by ID. Returns null if not found. */ export async function findFlyerById(id: number, client?: PoolClient): Promise { 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) ```typescript /** * List all active flyers. Returns empty array if none found. */ export async function listActiveFlyers(client?: PoolClient): Promise { 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](../adr/0028-api-response-standardization.md) ### Success Response ```typescript import { sendSuccess } from '../utils/apiResponse'; app.post('/api/v1/flyers', async (req, res) => { const flyer = await flyerService.createFlyer(req.body); // sendSuccess(res, data, statusCode?, meta?) return sendSuccess(res, flyer, 201); }); ``` ### Paginated Response ```typescript import { sendPaginated } from '../utils/apiResponse'; app.get('/api/v1/flyers', async (req, res) => { const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 20; const { items, total } = await flyerService.listFlyers(page, limit); // sendPaginated(res, data[], { page, limit, total }, meta?) return sendPaginated(res, items, { page, limit, total }); }); ``` ### Error Response ```typescript import { sendError, sendSuccess, ErrorCode } 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) { // sendError(res, code, message, statusCode?, details?, meta?) if (error instanceof NotFoundError) { return sendError(res, ErrorCode.NOT_FOUND, error.message, 404); } req.log.error({ error }, `Error in ${req.originalUrl.split('?')[0]}:`); return sendError(res, ErrorCode.INTERNAL_ERROR, 'An error occurred', 500); } }); ``` --- ## Transaction Management **ADR**: [ADR-002](../adr/0002-standardized-transaction-management.md) ### Basic Transaction ```typescript import { withTransaction } from '../services/db/connection.db'; export async function createFlyerWithItems( flyerData: FlyerInput, items: FlyerItemInput[], ): Promise { 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 ```typescript export async function bulkImportFlyers(flyersData: FlyerInput[]): Promise { 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](../adr/0003-standardized-input-validation-using-middleware.md) ### Zod Schema Definition ```typescript // 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; ``` ### Route Validation Middleware ```typescript import { validateRequest } from '../middleware/validation'; import { createFlyerSchema } from '../schemas/flyer.schemas'; app.post('/api/v1/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, 201); }); ``` ### Manual Validation ```typescript import { createFlyerSchema } from '../schemas/flyer.schemas'; export async function processFlyer(data: unknown): Promise { // Validate and parse input const validated = createFlyerSchema.parse(data); // Type-safe from here on return flyerDb.createFlyer(validated); } ``` --- ## Authentication **ADR**: [ADR-048](../adr/0048-authentication-strategy.md) ### Protected Route with JWT ```typescript import { authenticateJWT } from '../middleware/auth'; app.get( '/api/v1/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 ```typescript import { optionalAuth } from '../middleware/auth'; app.get( '/api/v1/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 ```typescript 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-009](../adr/0009-caching-strategy-for-read-heavy-operations.md) ### Cache Pattern ```typescript import { cacheService } from '../services/cache.server'; export async function getFlyer(id: number): Promise { // Try cache first const cached = await cacheService.get(`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 ```typescript export async function updateFlyer(id: number, data: UpdateFlyerInput): Promise { 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-006](../adr/0006-background-job-processing-and-task-queues.md) ### Queue Job ```typescript import { flyerProcessingQueue } from '../services/queues.server'; export async function enqueueFlyerProcessing(flyerId: number): Promise { await flyerProcessingQueue.add( 'process-flyer', { flyerId, timestamp: Date.now(), }, { attempts: 3, backoff: { type: 'exponential', delay: 2000, }, }, ); } ``` ### Process Job ```typescript // 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, }, ); ``` --- ## Feature Flags **ADR**: [ADR-024](../adr/0024-feature-flagging-strategy.md) Feature flags enable controlled feature rollout, A/B testing, and quick production disablement without redeployment. All flags default to `false` (opt-in model). ### Backend Usage ```typescript import { isFeatureEnabled, getFeatureFlags } from '../services/featureFlags.server'; // Check a specific flag in route handler router.get('/dashboard', async (req, res) => { if (isFeatureEnabled('newDashboard')) { return sendSuccess(res, { version: 'v2', data: await getNewDashboardData() }); } return sendSuccess(res, { version: 'v1', data: await getLegacyDashboardData() }); }); // Check flag in service layer function processFlyer(flyer: Flyer): ProcessedFlyer { if (isFeatureEnabled('experimentalAi')) { return processWithExperimentalAi(flyer); } return processWithStandardAi(flyer); } // Get all flags (admin endpoint) router.get('/admin/feature-flags', requireAdmin, async (req, res) => { sendSuccess(res, { flags: getFeatureFlags() }); }); ``` ### Frontend Usage ```tsx import { useFeatureFlag, useAllFeatureFlags } from '../hooks/useFeatureFlag'; import { FeatureFlag } from '../components/FeatureFlag'; // Hook approach - for logic beyond rendering function Dashboard() { const isNewDashboard = useFeatureFlag('newDashboard'); useEffect(() => { if (isNewDashboard) { analytics.track('new_dashboard_viewed'); } }, [isNewDashboard]); return isNewDashboard ? : ; } // Declarative component approach function App() { return ( }> ); } // Debug panel showing all flags function DebugPanel() { const flags = useAllFeatureFlags(); return (
    {Object.entries(flags).map(([name, enabled]) => (
  • {name}: {enabled ? 'ON' : 'OFF'}
  • ))}
); } ``` ### Adding a New Flag 1. **Backend** (`src/config/env.ts`): ```typescript // In featureFlagsSchema myNewFeature: booleanString(false), // FEATURE_MY_NEW_FEATURE // In loadEnvVars() myNewFeature: process.env.FEATURE_MY_NEW_FEATURE, ``` 2. **Frontend** (`src/config.ts` and `src/vite-env.d.ts`): ```typescript // In config.ts featureFlags section myNewFeature: import.meta.env.VITE_FEATURE_MY_NEW_FEATURE === 'true', // In vite-env.d.ts readonly VITE_FEATURE_MY_NEW_FEATURE?: string; ``` 3. **Environment** (`.env.example`): ```bash # FEATURE_MY_NEW_FEATURE=false # VITE_FEATURE_MY_NEW_FEATURE=false ``` ### Testing Feature Flags ```typescript // Backend - reset modules to test different states beforeEach(() => { vi.resetModules(); process.env.FEATURE_NEW_DASHBOARD = 'true'; }); // Frontend - mock config module vi.mock('../config', () => ({ default: { featureFlags: { newDashboard: true, betaRecipes: false, }, }, })); ``` ### Flag Lifecycle | Phase | Actions | | ---------- | -------------------------------------------------------------- | | **Add** | Add to schemas (backend + frontend), default `false`, document | | **Enable** | Set env var `='true'`, restart application | | **Remove** | Remove conditional code, remove from schemas, remove env vars | | **Sunset** | Max 3 months after full rollout - remove flag | ### Current Flags | Flag | Backend Env Var | Frontend Env Var | Purpose | | ---------------- | ------------------------- | ------------------------------ | ------------------------ | | `bugsinkSync` | `FEATURE_BUGSINK_SYNC` | `VITE_FEATURE_BUGSINK_SYNC` | Bugsink error sync | | `advancedRbac` | `FEATURE_ADVANCED_RBAC` | `VITE_FEATURE_ADVANCED_RBAC` | Advanced RBAC features | | `newDashboard` | `FEATURE_NEW_DASHBOARD` | `VITE_FEATURE_NEW_DASHBOARD` | New dashboard experience | | `betaRecipes` | `FEATURE_BETA_RECIPES` | `VITE_FEATURE_BETA_RECIPES` | Beta recipe features | | `experimentalAi` | `FEATURE_EXPERIMENTAL_AI` | `VITE_FEATURE_EXPERIMENTAL_AI` | Experimental AI features | | `debugMode` | `FEATURE_DEBUG_MODE` | `VITE_FEATURE_DEBUG_MODE` | Debug mode | --- ## Related Documentation - [ADR Index](../adr/index.md) - All architecture decision records - [TESTING.md](TESTING.md) - Testing patterns - [DEBUGGING.md](DEBUGGING.md) - Debugging strategies - [Database Guide](../subagents/DATABASE-GUIDE.md) - Database patterns - [Coder Reference](../SUBAGENT-CODER-REFERENCE.md) - Quick reference for AI agents