Prompt file imported from MrSimple263/Campaign-Manager (
.github/prompts/plan-cleanArchitectureRefactor.prompt.md). Copyright stays with the author.
Plan: Refactor Backend to Clean Architecture
TL;DR
Refactor the backend from current "fat controllers" to Clean Architecture with 3 layers: Controller → Service → Repository, organized by domain modules (auth, campaigns, recipients). This improves testability, separation of concerns, and maintainability.
Current State
- Controllers contain business logic + database queries
- Query helpers exist in
db/queries/but are unused - Only 1 service exists (
emailService.ts) - 3 logical domains: Auth, Campaigns, Recipients
Target Architecture
src/
├── modules/
│ ├── auth/
│ │ ├── auth.controller.ts # HTTP handling only
│ │ ├── auth.service.ts # Business logic
│ │ ├── auth.repository.ts # Data access
│ │ ├── auth.routes.ts # Route definitions
│ │ └── auth.types.ts # Module-specific types
│ ├── campaigns/
│ │ ├── campaign.controller.ts
│ │ ├── campaign.service.ts
│ │ ├── campaign.repository.ts
│ │ ├── campaign.routes.ts
│ │ └── campaign.types.ts
│ └── recipients/
│ ├── recipient.controller.ts
│ ├── recipient.service.ts
│ ├── recipient.repository.ts
│ ├── recipient.routes.ts
│ └── recipient.types.ts
├── shared/
│ ├── middleware/
│ ├── utils/
│ ├── constants/
│ └── types/
├── config/
├── db/
│ └── index.ts # Knex instance only
├── app.ts
└── index.ts
Steps
Phase 1: Create Module Structure
- Create
src/modules/folder with subfolders:auth/,campaigns/,recipients/ - Move shared code to
src/shared/(middleware, utils, constants)
Phase 2: Auth Module
- Create
auth.repository.ts— move user queries from controllerfindByEmail(email),findById(id),create(data),isEmailTaken(email)
- Create
auth.service.ts— extract business logicregister(input),login(input),getCurrentUser(userId)- JWT generation, password hashing calls
- Refactor
auth.controller.ts— HTTP handling only- Parse request, call service, format response
- Move
auth.routes.tsto module folder - Create
auth.types.ts— module-specific interfaces
Phase 3: Recipients Module (parallel with Phase 2)
- Create
recipient.repository.tsfindById(id),findByEmail(email),findMany(options),create(data),findByIds(ids)
- Create
recipient.service.tsgetRecipients(options),createRecipient(input)
- Refactor
recipient.controller.ts - Move
recipient.routes.tsto module folder
Phase 4: Campaigns Module (depends on Phase 3 for recipient repo)
- Create
campaign.repository.tsfindById(id),findByIdAndUser(id, userId),findMany(options),create(data),update(id, data),delete(id)- Campaign recipient methods:
addRecipients(),clearRecipients(),getRecipients(),getStats()
- Create
campaign.service.tsgetCampaigns(userId, options),getCampaign(id, userId),createCampaign(userId, input),updateCampaign(id, userId, input),deleteCampaign(id, userId),scheduleCampaign(id, userId, scheduledAt),sendCampaign(id, userId)- Business rules: draft-only edit/delete, future-only scheduling
- Move
emailService.ts→campaign.service.tsor keep as shared service - Refactor
campaign.controller.ts - Move
campaign.routes.tsto module folder
Phase 5: Wire Up & Cleanup
- Update
src/routes/index.tsto import from modules - Delete old
src/controllers/,src/routes/auth|campaigns|recipients.ts - Delete unused
src/db/queries/(replaced by repositories) - Update imports across the codebase
Relevant Files
To Create
src/modules/auth/auth.controller.tssrc/modules/auth/auth.service.tssrc/modules/auth/auth.repository.tssrc/modules/auth/auth.routes.tssrc/modules/auth/auth.types.tssrc/modules/campaigns/campaign.controller.tssrc/modules/campaigns/campaign.service.tssrc/modules/campaigns/campaign.repository.tssrc/modules/campaigns/campaign.routes.tssrc/modules/campaigns/campaign.types.tssrc/modules/recipients/recipient.controller.tssrc/modules/recipients/recipient.service.tssrc/modules/recipients/recipient.repository.tssrc/modules/recipients/recipient.routes.tssrc/modules/recipients/recipient.types.tssrc/shared/— move middleware, utils, constants
To Delete
src/controllers/— replaced by module controllerssrc/routes/auth.ts,campaigns.ts,recipients.ts— moved to modulessrc/db/queries/— replaced by repositories
To Modify
src/routes/index.ts— import from modulessrc/app.ts— may need minor adjustments
Verification
- TypeScript Build:
yarn workspace backend build— no errors - Tests Pass:
yarn workspace backend test— all tests pass - API Unchanged: Same endpoints, same request/response format
- Manual Test: Register → Login → Create campaign → Send
Decisions
- Repository returns raw data, service transforms if needed
- Services throw AppError for business rule violations
- Controllers only handle HTTP (req parsing, res formatting, status codes)
- Keep emailService separate (used internally by campaign.service)
- One repository per aggregate root (User, Campaign, Recipient)
- CampaignRepository handles campaign_recipients (same aggregate)
Layer Responsibilities
| Layer | Responsibilities | Does NOT do |
|---|---|---|
| Controller | Parse request, call service, format response, HTTP status | Business logic, DB queries |
| Service | Business rules, orchestration, validation beyond schema | HTTP handling, raw SQL |
| Repository | Data access, Knex queries, entity mapping | Business logic, HTTP |
Example Flow: Create Campaign
Request → Controller.createCampaign()
↓
Service.createCampaign(userId, input)
├─→ Validate recipient IDs exist (recipientRepo.findByIds)
├─→ Create campaign (campaignRepo.create)
└─→ Add recipients (campaignRepo.addRecipients)
↓
Return campaign with recipients
↓
Response ← Controller formats JSON response