All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 22m13s
17 KiB
17 KiB
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 |
|---|---|---|
| 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 |
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
- Error Handling
- Repository Patterns
- API Response Patterns
- Transaction Management
- Input Validation
- Authentication
- Caching
- Background Jobs
- Feature Flags
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/v1/flyers', async (req, res) => {
const flyer = await flyerService.createFlyer(req.body);
// sendSuccess(res, data, statusCode?, meta?)
return sendSuccess(res, flyer, 201);
});
Paginated Response
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
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
Basic Transaction
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
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/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
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/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
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
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
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-006
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,
},
);
Feature Flags
ADR: ADR-024
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
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
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
-
Backend (
src/config/env.ts):// In featureFlagsSchema myNewFeature: booleanString(false), // FEATURE_MY_NEW_FEATURE // In loadEnvVars() myNewFeature: process.env.FEATURE_MY_NEW_FEATURE, -
Frontend (
src/config.tsandsrc/vite-env.d.ts):// 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; -
Environment (
.env.example):# FEATURE_MY_NEW_FEATURE=false # VITE_FEATURE_MY_NEW_FEATURE=false
Testing Feature Flags
// 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 - All architecture decision records
- TESTING.md - Testing patterns
- DEBUGGING.md - Debugging strategies
- Database Guide - Database patterns
- Coder Reference - Quick reference for AI agents