# Backend Module Architecture [← Back to Index](../CLAUDE.md) --- ## Standard Module Structure All feature modules in `/backend/modules/` follow a consistent architecture pattern. This ensures code is organized, maintainable, and easy to navigate. ### Typical Module Directory ``` /backend/modules/[module-name]/ ├── routes.js # Express router with endpoint definitions ├── repository.js # Data access layer (Sequelize queries) ├── operations/ # Business logic operations (optional) │ ├── create.js │ ├── update.js │ ├── delete.js │ └── ... ├── queries/ # Complex query builders (optional) ├── core/ # Core utilities (optional) │ ├── serializers.js # Format data for API responses │ ├── parsers.js # Parse request data │ └── builders.js # Build database objects ├── middleware/ # Route-specific middleware (optional) │ └── access.js # Access control └── utils/ # Module-specific utilities └── validation.js # Input validation ``` **Note:** Not all modules have all directories. Simpler modules may only have `routes.js` and basic logic. --- ## Example: Tasks Module (Complex) The tasks module (`/backend/modules/tasks/`) is the most comprehensive example, showing the full potential of the module pattern: ``` /backend/modules/tasks/ ├── routes.js # Main routes: GET /tasks, POST /task, etc. ├── repository.js # Data access: findTaskById, findTasksForUser ├── recurringTaskService.js # Recurring task pattern logic (8.5KB) ├── taskEventService.js # Task activity logging ├── taskScheduler.js # Cron-based task scheduling with node-cron │ ├── operations/ # Business logic operations │ ├── list.js # List operations and filtering │ ├── completion.js # Task completion/status changes │ ├── recurring.js # Recurrence pattern handling │ ├── subtasks.js # Subtask CRUD operations │ ├── tags.js # Tag assignment to tasks │ ├── grouping.js # Task grouping logic │ ├── sorting.js # Sort order logic │ └── parent-child.js # Parent-child relationship handling │ ├── queries/ │ ├── query-builders.js # filterTasksByParams, buildWhereClause │ ├── metrics-queries.js # Task metrics and analytics queries │ └── metrics-computation.js # Metric calculations and aggregations │ ├── core/ │ ├── serializers.js # serializeTask, serializeTasks │ ├── builders.js # buildTaskAttributes for create/update │ ├── parsers.js # parseTaskInput from requests │ └── comparators.js # Detect task changes for audit log │ ├── middleware/ │ └── access.js # Task-specific access control │ └── utils/ ├── constants.js # Task-specific constants (status codes, etc.) ├── validation.js # Task input validation rules └── logging.js # Change tracking helpers ``` --- ## Example: Projects Module (Simpler) The projects module (`/backend/modules/projects/`) follows the same pattern but with less complexity: ``` /backend/modules/projects/ ├── routes.js # Project CRUD endpoints ├── repository.js # Project data access (findAll, findById, create, etc.) └── utils/ └── validation.js # Project validation (name required, etc.) ``` This shows that modules scale based on complexity - simple features don't require the full directory structure. --- ## Module Pattern Details ### routes.js - Express Router **Purpose:** Define HTTP endpoints for the module **Pattern:** ```javascript // /backend/modules/[module]/routes.js const express = require('express'); const router = express.Router(); const repository = require('./repository'); const { hasAccess } = require('../../middleware/authorize'); // List all resources (collection) router.get('/[resources]', async (req, res, next) => { try { const items = await repository.findAll(req.currentUser.id, req.query); res.json(items); } catch (error) { next(error); // Pass to global error handler } }); // Create new resource (singular) router.post('/[resource]', async (req, res, next) => { try { const item = await repository.create(req.body, req.currentUser.id); res.status(201).json(item); } catch (error) { next(error); } }); // Get single resource (with authorization) router.get('/[resource]/:id', hasAccess('ro', '[resource]', (req) => req.params.id), async (req, res, next) => { try { const item = await repository.findById(req.params.id, req.currentUser.id); if (!item) { return res.status(404).json({ error: 'Not found' }); } res.json(item); } catch (error) { next(error); } } ); // Update resource (with authorization) router.put('/[resource]/:id', hasAccess('rw', '[resource]', (req) => req.params.id), async (req, res, next) => { try { const updated = await repository.update(req.params.id, req.body, req.currentUser.id); res.json(updated); } catch (error) { next(error); } } ); // Delete resource router.delete('/[resource]/:id', hasAccess('rw', '[resource]', (req) => req.params.id), async (req, res, next) => { try { await repository.destroy(req.params.id, req.currentUser.id); res.status(204).send(); } catch (error) { next(error); } } ); module.exports = router; ``` **Key Conventions:** - Plural for collections: `GET /tasks` - Singular for single resource: `GET /task/:id`, `POST /task` - Always use try/catch - Pass errors to `next(error)` for global error handler - Use `hasAccess()` middleware for authorization - Return proper HTTP status codes (200, 201, 204, 404, etc.) --- ### repository.js - Data Access Layer **Purpose:** Abstract database queries from routes **Pattern:** ```javascript // /backend/modules/[module]/repository.js const { Model } = require('../../models'); async function findAll(userId, filters = {}) { return await Model.findAll({ where: { user_id: userId, ...buildWhereClause(filters) }, include: [...], order: [['created_at', 'DESC']] }); } async function findById(id, userId) { return await Model.findOne({ where: { id, user_id: userId }, include: [...] }); } async function create(data, userId) { return await Model.create({ ...data, user_id: userId }); } async function update(id, data, userId) { const instance = await findById(id, userId); if (!instance) { throw new NotFoundError('Resource not found'); } return await instance.update(data); } async function destroy(id, userId) { const instance = await findById(id, userId); if (!instance) { throw new NotFoundError('Resource not found'); } await instance.destroy(); } module.exports = { findAll, findById, create, update, destroy }; ``` **Why Repository Pattern:** - Separates data access from business logic - Makes testing easier (can mock repository) - Centralizes query logic - Prevents Model usage directly in routes --- ### core/serializers.js - Response Formatting **Purpose:** Transform database objects into API-friendly format **Pattern:** ```javascript // /backend/modules/[module]/core/serializers.js function serializeItem(item) { if (!item) return null; return { id: item.id, uid: item.uid, name: item.name, description: item.description, created_at: item.created_at, updated_at: item.updated_at, // Include associations if loaded tags: item.Tags ? item.Tags.map(serializeTag) : undefined, user: item.User ? serializeUser(item.User) : undefined }; } function serializeItems(items) { return items.map(serializeItem); } module.exports = { serializeItem, serializeItems }; ``` **Usage in routes:** ```javascript const { serializeItem, serializeItems } = require('./core/serializers'); router.get('/items', async (req, res) => { const items = await repository.findAll(req.currentUser.id); res.json(serializeItems(items)); // Transform before sending }); ``` --- ### core/builders.js - Object Construction **Purpose:** Build objects for database creation/update from request data **Pattern:** ```javascript // /backend/modules/[module]/core/builders.js function buildItemAttributes(data, userId) { const attributes = { user_id: userId, name: data.name?.trim(), description: data.description?.trim() || null }; // Optional fields if (data.due_date) { attributes.due_date = parseDate(data.due_date); } if (data.priority !== undefined) { attributes.priority = parseInt(data.priority, 10); } return attributes; } module.exports = { buildItemAttributes }; ``` **Usage:** ```javascript const { buildItemAttributes } = require('./core/builders'); router.post('/item', async (req, res) => { const attributes = buildItemAttributes(req.body, req.currentUser.id); const item = await repository.create(attributes); res.status(201).json(serializeItem(item)); }); ``` --- ### operations/ - Business Logic **Purpose:** Complex operations that don't fit in simple CRUD **Example: operations/completion.js (from tasks module)** ```javascript // /backend/modules/tasks/operations/completion.js async function completeTask(taskId, userId, completionData) { // Get task with associations const task = await repository.findById(taskId, userId); if (task.recurrence_type) { // Handle recurring task completion await recurringTaskService.handleCompletion(task, completionData); } else { // Simple completion task.status = 2; // completed task.completed_at = new Date(); await task.save(); } // Log event await taskEventService.logEvent(taskId, 'completed', userId); // Update related subtasks if (completionData.completeSubtasks) { await completeSubtasks(taskId); } return task; } module.exports = { completeTask }; ``` --- ## Module Communication ### Accessing Other Modules Modules can import other module repositories and services: ```javascript // In /backend/modules/projects/routes.js // Import task repository from tasks module const taskRepository = require('../tasks/repository'); // Import shared service const permissionsService = require('../../services/permissionsService'); router.get('/project/:id/tasks', async (req, res) => { // Use task repository const tasks = await taskRepository.findTasksForProject(req.params.id); res.json(tasks); }); ``` ### Avoid Circular Dependencies **Bad:** ```javascript // Module A imports Module B const moduleB = require('../moduleB'); // Module B imports Module A const moduleA = require('../moduleA'); // CIRCULAR! ``` **Good:** ```javascript // Extract shared logic to /backend/services/ // Both modules import from services const sharedService = require('../../services/sharedService'); ``` --- ## How to Add a New Module **Example: Creating a "labels" module** ### Step 1: Create Directory Structure ```bash mkdir -p /Users/chris/c0deLab/ProjectLand/tududi/backend/modules/labels mkdir -p /Users/chris/c0deLab/ProjectLand/tududi/backend/modules/labels/utils ``` ### Step 2: Create routes.js ```javascript // /backend/modules/labels/routes.js const express = require('express'); const router = express.Router(); const repository = require('./repository'); router.get('/labels', async (req, res, next) => { try { const labels = await repository.findAll(req.currentUser.id); res.json(labels); } catch (error) { next(error); } }); router.post('/label', async (req, res, next) => { try { const label = await repository.create(req.body, req.currentUser.id); res.status(201).json(label); } catch (error) { next(error); } }); router.put('/label/:id', async (req, res, next) => { try { const label = await repository.update(req.params.id, req.body, req.currentUser.id); res.json(label); } catch (error) { next(error); } }); router.delete('/label/:id', async (req, res, next) => { try { await repository.destroy(req.params.id, req.currentUser.id); res.status(204).send(); } catch (error) { next(error); } }); module.exports = router; ``` ### Step 3: Create repository.js ```javascript // /backend/modules/labels/repository.js const { Label } = require('../../models'); async function findAll(userId) { return await Label.findAll({ where: { user_id: userId }, order: [['name', 'ASC']] }); } async function findById(id, userId) { return await Label.findOne({ where: { id, user_id: userId } }); } async function create(data, userId) { return await Label.create({ name: data.name, color: data.color || '#gray', user_id: userId }); } async function update(id, data, userId) { const label = await findById(id, userId); if (!label) { throw new Error('Label not found'); } return await label.update({ name: data.name, color: data.color }); } async function destroy(id, userId) { const label = await findById(id, userId); if (!label) { throw new Error('Label not found'); } await label.destroy(); } module.exports = { findAll, findById, create, update, destroy }; ``` ### Step 4: Create Model See [Database documentation](database.md) for creating models and migrations. ### Step 5: Register Routes in app.js Edit `/backend/app.js`: ```javascript // Add with other module registrations (around line 50-70) // Labels module app.use('/api/v1', require('./modules/labels/routes')); app.use('/api', require('./modules/labels/routes')); // Backward compatibility ``` ### Step 6: Add Swagger Documentation Edit `/backend/config/swagger.js` to add Label schema: ```javascript components: { schemas: { // ... existing schemas ... Label: { type: 'object', properties: { id: { type: 'integer' }, uid: { type: 'string' }, name: { type: 'string' }, color: { type: 'string' }, created_at: { type: 'string', format: 'date-time' }, updated_at: { type: 'string', format: 'date-time' } } } } } ``` ### Step 7: Write Tests Create `/backend/tests/integration/labels/labels.test.js`: ```javascript const request = require('supertest'); const app = require('../../../app'); const { Label, User } = require('../../../models'); describe('Labels API', () => { let user, authCookie; beforeEach(async () => { user = await User.create({ email: 'test@example.com', password: 'password123' }); const res = await request(app) .post('/api/login') .send({ email: 'test@example.com', password: 'password123' }); authCookie = res.headers['set-cookie']; }); afterEach(async () => { await Label.destroy({ where: {} }); await User.destroy({ where: {} }); }); it('should create label', async () => { const response = await request(app) .post('/api/v1/label') .set('Cookie', authCookie) .send({ name: 'Important', color: '#red' }); expect(response.status).toBe(201); expect(response.body.name).toBe('Important'); }); it('should list user labels', async () => { await Label.create({ name: 'Label 1', user_id: user.id }); await Label.create({ name: 'Label 2', user_id: user.id }); const response = await request(app) .get('/api/v1/labels') .set('Cookie', authCookie); expect(response.status).toBe(200); expect(response.body.length).toBe(2); }); }); ``` ### Step 8: Create Frontend Integration (Optional) Create `/frontend/utils/labelsService.ts` and components as needed. --- ## Module Checklist When adding a new module, ensure: - [ ] Directory created in `/backend/modules/[name]/` - [ ] `routes.js` with all necessary endpoints - [ ] `repository.js` with data access methods - [ ] Model created (if new database table needed) - [ ] Migration created and run - [ ] Routes registered in `/backend/app.js` - [ ] Swagger schema added - [ ] Integration tests written - [ ] Documentation updated (if public feature) --- [← Back to Index](../CLAUDE.md)