tududi/docs/backend-patterns.md
Chris 3486541272
Add comprehensive LLM development documentation (#939)
* 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
2026-03-14 02:54:59 +02:00

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)