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

16 KiB

Backend Module Architecture

← Back to Index


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:

// /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:

// /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:

// /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:

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:

// /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:

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)

// /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:

// 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:

// Module A imports Module B
const moduleB = require('../moduleB');

// Module B imports Module A
const moduleA = require('../moduleA'); // CIRCULAR!

Good:

// 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

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

// /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

// /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 for creating models and migrations.

Step 5: Register Routes in app.js

Edit /backend/app.js:

// 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:

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:

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