# 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<ErrorResponse>(404, 'Item not found')
  public async getItem(@Path() id: number): Promise<SuccessResponseType<ItemResponse>> {
    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<ErrorResponse>(401, 'Not authenticated')
  public async createItem(
    @Body() body: CreateItemRequest,
    @Request() request: ExpressRequest,
  ): Promise<SuccessResponseType<ItemResponse>> {
    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<void> {
  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<ErrorResponse>(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<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

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

```typescript
/**
 * 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)

```typescript
/**
 * 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](../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<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

```typescript
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](../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<typeof createFlyerSchema>;
```

### 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<Flyer> {
  // 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<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

```typescript
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-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<void> {
  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 ? <NewDashboard /> : <LegacyDashboard />;
}

// Declarative component approach
function App() {
  return (
    <FeatureFlag feature="newDashboard" fallback={<LegacyDashboard />}>
      <NewDashboard />
    </FeatureFlag>
  );
}

// Debug panel showing all flags
function DebugPanel() {
  const flags = useAllFeatureFlags();
  return (
    <ul>
      {Object.entries(flags).map(([name, enabled]) => (
        <li key={name}>
          {name}: {enabled ? 'ON' : 'OFF'}
        </li>
      ))}
    </ul>
  );
}
```

### 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
