479 lines
11 KiB
Markdown
479 lines
11 KiB
Markdown
# Code Patterns
|
|
|
|
Common code patterns extracted from Architecture Decision Records (ADRs). Use these as templates when writing new code.
|
|
|
|
## Table of Contents
|
|
|
|
- [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)
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
**ADR**: [ADR-001](../adr/0001-standardized-error-handling-for-database-operations.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/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);
|
|
}
|
|
});
|
|
```
|
|
|
|
### 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-layer-method-naming-conventions.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-consistent-api-response-format.md)
|
|
|
|
### Success Response
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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](../adr/0002-transaction-management-pattern.md)
|
|
|
|
### Basic Transaction
|
|
|
|
```typescript
|
|
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
|
|
|
|
```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-input-validation-framework.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/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
|
|
|
|
```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/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/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-029](../adr/0029-redis-caching-strategy.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-036](../adr/0036-background-job-processing-architecture.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,
|
|
},
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|