# Coder Subagent Reference ## Quick Navigation | Category | Key Files | | ------------ | ------------------------------------------------------------------ | | Routes | `src/routes/*.routes.ts` | | Services | `src/services/*.server.ts` (backend), `src/services/*.ts` (shared) | | Repositories | `src/services/db/*.db.ts` | | Types | `src/types.ts`, `src/types/*.ts` | | Schemas | `src/schemas/*.schemas.ts` | | Config | `src/config/env.ts` | | Utils | `src/utils/*.ts` | --- ## Architecture Patterns (ADR Summary) ### Layer Flow ``` Route → validateRequest(schema) → Service → Repository → Database ↓ External APIs ``` ### Repository Naming Convention (ADR-034) | Prefix | Behavior | Return | | --------- | --------------------------------- | -------------- | | `get*` | Throws `NotFoundError` if missing | Entity | | `find*` | Returns `null` if missing | Entity \| null | | `list*` | Returns empty array if none | Entity[] | | `create*` | Creates new record | Entity | | `update*` | Updates existing | Entity | | `delete*` | Removes record | void | ### Error Handling (ADR-001) ```typescript import { handleDbError, NotFoundError } from '../services/db/errors.db'; import { logger } from '../services/logger.server'; // Repository pattern async function getById(id: string): Promise { try { const result = await pool.query('SELECT * FROM table WHERE id = $1', [id]); if (result.rows.length === 0) throw new NotFoundError('Entity not found'); return result.rows[0]; } catch (error) { handleDbError(error, logger, 'Failed to get entity', { id }); } } ``` ### API Response Helpers (ADR-028) ```typescript import { sendSuccess, sendPaginated, sendError, sendNoContent, sendMessage, ErrorCode, } from '../utils/apiResponse'; // Success with data sendSuccess(res, data); // 200 sendSuccess(res, data, 201); // 201 Created // Paginated sendPaginated(res, items, { page, limit, total }); // Error sendError(res, ErrorCode.NOT_FOUND, 'User not found', 404); sendError(res, ErrorCode.VALIDATION_ERROR, 'Invalid input', 400, validationErrors); // No content / Message sendNoContent(res); // 204 sendMessage(res, 'Password updated'); ``` ### Transaction Pattern (ADR-002) ```typescript import { withTransaction } from '../services/db/connection.db'; const result = await withTransaction(async (client) => { await client.query('INSERT INTO a ...'); await client.query('INSERT INTO b ...'); return { success: true }; }); ``` --- ## Adding New Features ### New API Endpoint Checklist 1. **Schema** (`src/schemas/{domain}.schemas.ts`) ```typescript import { z } from 'zod'; export const createEntitySchema = z.object({ body: z.object({ name: z.string().min(1) }), }); ``` 2. **Route** (`src/routes/{domain}.routes.ts`) ```typescript import { validateRequest } from '../middleware/validation.middleware'; import { createEntitySchema } from '../schemas/{domain}.schemas'; router.post('/', validateRequest(createEntitySchema), async (req, res, next) => { try { const result = await entityService.create(req.body); sendSuccess(res, result, 201); } catch (error) { next(error); } }); ``` 3. **Service** (`src/services/{domain}Service.server.ts`) ```typescript export async function create(data: CreateInput): Promise { // Business logic here return repository.create(data); } ``` 4. **Repository** (`src/services/db/{domain}.db.ts`) ```typescript export async function create(data: CreateInput, client?: PoolClient): Promise { const pool = client || getPool(); try { const result = await pool.query('INSERT INTO ...', [data.name]); return result.rows[0]; } catch (error) { handleDbError(error, logger, 'Failed to create entity', { data }); } } ``` ### New Background Job Checklist 1. **Queue** (`src/services/queues.server.ts`) ```typescript export const myQueue = new Queue('my-queue', { connection: redisConnection }); ``` 2. **Worker** (`src/services/workers.server.ts`) ```typescript new Worker( 'my-queue', async (job) => { // Process job }, { connection: redisConnection }, ); ``` 3. **Trigger** (in service) ```typescript await myQueue.add('job-name', { data }); ``` --- ## Key Files Reference ### Database Repositories | Repository | Purpose | Path | | -------------------- | ----------------------------- | ------------------------------------ | | `flyer.db.ts` | Flyer CRUD, processing status | `src/services/db/flyer.db.ts` | | `store.db.ts` | Store management | `src/services/db/store.db.ts` | | `user.db.ts` | User accounts | `src/services/db/user.db.ts` | | `shopping.db.ts` | Shopping lists, watchlists | `src/services/db/shopping.db.ts` | | `gamification.db.ts` | Achievements, points | `src/services/db/gamification.db.ts` | | `category.db.ts` | Item categories | `src/services/db/category.db.ts` | | `price.db.ts` | Price history, comparisons | `src/services/db/price.db.ts` | | `recipe.db.ts` | Recipe management | `src/services/db/recipe.db.ts` | ### Services | Service | Purpose | Path | | ---------------------------------- | -------------------------------- | ----------------------------------------------- | | `flyerProcessingService.server.ts` | Orchestrates flyer AI extraction | `src/services/flyerProcessingService.server.ts` | | `flyerAiProcessor.server.ts` | Gemini AI integration | `src/services/flyerAiProcessor.server.ts` | | `cacheService.server.ts` | Redis caching | `src/services/cacheService.server.ts` | | `queues.server.ts` | BullMQ queue definitions | `src/services/queues.server.ts` | | `workers.server.ts` | BullMQ workers | `src/services/workers.server.ts` | | `emailService.server.ts` | Nodemailer integration | `src/services/emailService.server.ts` | | `geocodingService.server.ts` | Address geocoding | `src/services/geocodingService.server.ts` | ### Routes | Route | Base Path | Auth Required | | ------------------ | ------------- | ------------- | | `flyer.routes.ts` | `/api/flyers` | Mixed | | `store.routes.ts` | `/api/stores` | Mixed | | `user.routes.ts` | `/api/users` | Yes | | `auth.routes.ts` | `/api/auth` | No | | `admin.routes.ts` | `/api/admin` | Admin only | | `deals.routes.ts` | `/api/deals` | No | | `health.routes.ts` | `/api/health` | No | --- ## Error Types | Error Class | HTTP Status | Use Case | | --------------------------- | ----------- | ------------------------- | | `NotFoundError` | 404 | Resource not found | | `ForbiddenError` | 403 | Access denied | | `ValidationError` | 400 | Input validation failed | | `UniqueConstraintError` | 409 | Duplicate record | | `ForeignKeyConstraintError` | 400 | Referenced record missing | | `NotNullConstraintError` | 400 | Required field null | Import: `import { NotFoundError, ... } from '../services/db/errors.db'` --- ## Middleware | Middleware | Purpose | Usage | | ------------------------- | -------------------- | ------------------------------------------------------------ | | `validateRequest(schema)` | Zod validation | `router.post('/', validateRequest(schema), handler)` | | `requireAuth` | JWT authentication | `router.get('/', requireAuth, handler)` | | `requireAdmin` | Admin role check | `router.delete('/', requireAuth, requireAdmin, handler)` | | `fileUpload` | Multer file handling | `router.post('/upload', fileUpload.single('file'), handler)` | --- ## Type Definitions | File | Contains | | --------------------------- | ----------------------------------------------- | | `src/types.ts` | Main types: User, Flyer, FlyerItem, Store, etc. | | `src/types/api.ts` | API response envelopes, pagination | | `src/types/auth.ts` | Auth-related types | | `src/types/gamification.ts` | Achievement types | --- ## Naming Conventions (ADR-027) | Context | Convention | Example | | ----------------- | ---------------- | ----------------------------------- | | AI output types | `Ai*` prefix | `AiFlyerItem`, `AiExtractionResult` | | Database types | `Db*` prefix | `DbFlyer`, `DbUser` | | API types | No prefix | `Flyer`, `User` | | Schema validation | `*Schema` suffix | `createFlyerSchema` | | Routes | `*.routes.ts` | `flyer.routes.ts` | | Repositories | `*.db.ts` | `flyer.db.ts` | | Server services | `*.server.ts` | `aiService.server.ts` | | Client services | `*.client.ts` | `logger.client.ts` |