* Increase coverage * Add comprehensive LLM development documentation - Add CLAUDE.md as main documentation index - Create 8 detailed documentation files in docs/: - architecture.md: Tech stack, data models, auth system - directory-structure.md: Complete file tree with paths - backend-patterns.md: Module architecture and patterns - database.md: Models, migrations, and workflows - development-workflow.md: Setup and daily development - code-conventions.md: Style guide and best practices - testing.md: Test organization and patterns - common-tasks.md: How-to guides for frequent tasks - Update .gitignore to allow project-level CLAUDE.md - 4,285 lines of comprehensive documentation - Organized for easy navigation with cross-links - LLM-optimized with absolute paths and code examples * fixup! Add comprehensive LLM development documentation
649 lines
16 KiB
Markdown
649 lines
16 KiB
Markdown
# 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)
|