Files
youtube-studio-flow/documentation/YouTube Studio Flow/04 - Design Guidelines/04 - Backend Architecture Patterns.md
T

252 lines
6.9 KiB
Markdown

# 04 - Backend Architecture Patterns
Established patterns for the NestJS backend of YouTube Studio Flow. These rules define how modules, services, and controllers are structured. For frontend patterns, see [[03 - Component Patterns]].
---
## Module structure
Every feature module follows this layout:
```
modules/feature-name/
feature-name.module.ts
feature-name.controller.ts
feature-name.service.ts
dto/
create-feature.dto.ts
update-feature.dto.ts
```
The `dto/` folder is optional for read-only modules, but required for any module that accepts request bodies.
---
## Controller responsibilities
Controllers handle routing and guards only. They must not contain business logic.
**What belongs in a controller:**
- Route decorators (`@Get`, `@Post`, `@Patch`, `@Delete`)
- Guard declarations (`@UseGuards`)
- Role decorators (`@Roles`)
- Extracting `req.user.id` and `req.user.teamId`
- Calling one service method and returning the result
**What does not belong in a controller:**
- Database queries
- Conditional logic
- Transformations beyond passing arguments
### Auth pattern
```typescript
@UseGuards(JwtAuthGuard)
@Controller('blocks')
export class BlocksController {
// Read operations — JwtAuthGuard on class is sufficient
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(TeamRole.EDITOR)
@Post()
create(@Request() req, @Body() dto: CreateBlockDto) {
return this.blocksService.create(req.user.teamId, req.user.id, dto);
}
}
```
- `JwtAuthGuard` on the class: all routes require authentication
- `RolesGuard` + `@Roles(TeamRole.EDITOR)` on write methods only
---
## Service responsibilities
All business logic lives in services.
**What belongs in a service:**
- All database access via `PrismaService`
- Team scoping on every query
- Audit logging for every mutation
- Queue enqueue calls
- Validation that requires database state
Services receive `teamId` and `actorId` as arguments from the controller — they never extract these from a request object.
---
## Team scoping — the cardinal rule
Every database query must be scoped to the current team. The ownership chain is:
```
Team → Channel → Video
```
For video queries, the scope travels through the channel:
```typescript
// CORRECT
await this.prisma.video.findMany({
where: { channel: { teamId } },
});
// WRONG — missing team scope
await this.prisma.video.findMany({
where: { id: videoId },
});
```
For resources owned directly by a team (blocks, templates, collaborators, variables):
```typescript
await this.prisma.descriptionBlock.findMany({
where: { teamId },
});
```
Violating team scoping is a data leak between tenants. There are no exceptions.
---
## Audit logging
Required for every user-facing mutation: create, update, and delete. Background/system operations (queue processors, scheduled jobs) do not get audit logs.
**Setup — in the module:**
```typescript
@Module({
imports: [AuditModule, PrismaModule],
controllers: [BlocksController],
providers: [BlocksService],
})
export class BlocksModule {}
```
**Usage — in the service:**
```typescript
constructor(
private readonly prisma: PrismaService,
private readonly audit: AuditService,
) {}
async update(teamId: string, actorId: string, id: string, dto: UpdateBlockDto) {
const before = await this.prisma.descriptionBlock.findUnique({ where: { id } });
const after = await this.prisma.descriptionBlock.update({
where: { id },
data: dto,
});
await this.audit.log(actorId, 'DescriptionBlock', id, 'update', before, after);
return after;
}
```
Tracked entity types: `Video`, `VideoConfig`, `DescriptionBlock`, `Template`, `Collaborator`, `TeamVariable`, `SavedView`, `TeamMember`.
---
## Render engine usage
Never fetch blocks, variables, or collaborators manually to construct a render. Always go through `VideoRenderService`:
```typescript
// In module imports:
// VideoRenderModule (re-exports VideoRenderService)
constructor(private readonly videoRenderService: VideoRenderService) {}
const { rendered, hash } = await this.videoRenderService.render(videoId);
```
`VideoRenderService` is the single source of truth for fetching render data and delegating to `RenderEngineService`. The only exceptions are `video-configs.service.ts` and `templates.service.ts`, which may call `RenderEngineService` directly for preview rendering because they already hold all the data.
See the CLAUDE.md render engine section for the full block type behavior reference.
---
## YouTube API quota
All YouTube write operations cost quota. `videos.update` costs 50 units. The daily budget is 10,000 units, resetting at midnight Pacific Time.
Required pattern before every YouTube write:
```typescript
const ok = await this.quotaService.canSpend(50);
if (!ok) throw new Error('Quota exceeded');
await this.quotaService.spend(50, 'videos.update', { videoId, channelId });
// ... make the YouTube API call
```
Never make a YouTube write call without checking quota first.
---
## Queue enqueue pattern
Jobs are enqueued with a deterministic `jobId` to prevent duplicate queuing:
```typescript
// Standard enqueue — deduplicates by videoId:
await this.lintQueue.add('lint', { videoId }, {
jobId: `lint-${videoId}`,
});
// Forced rerun — bypass deduplication:
await this.lintQueue.add('lint', { videoId }, {
jobId: `lint-${videoId}-${Date.now()}`,
});
```
Queue names are defined in a `QUEUES` constant. Job name strings use kebab-case: `'lint'`, `'youtube-sync'`, `'render'`.
---
## Prisma enum rule
Never remove a value from a Prisma enum. Removing an enum value in PostgreSQL requires a raw SQL migration and risks data loss if existing rows reference the removed value.
When a concept is retired from the UI, mark it as deprecated in comments and hide it from the frontend. Leave the enum value in the schema.
Current example: `BlockType.GLOBAL` and `BlockType.REPEATABLE` are removed from the UI but remain in the DB enum.
---
## Schema change workflow
After any change to `backend/prisma/schema.prisma`:
```bash
# 1. Stop the running backend process
npx prisma generate # regenerates the Prisma client
npx prisma migrate deploy # applies pending migrations
# 2. Restart the backend
```
Run these from the `backend/` directory. Both the API process (`src/main.ts`) and the worker process (`src/worker.ts`) must be restarted.
---
## Error handling
Use NestJS built-in HTTP exceptions at service boundaries. Do not add generic `try/catch` blocks for errors that cannot occur.
| Situation | Exception |
|---|---|
| Entity not found | `NotFoundException` |
| Team scoping violation | `ForbiddenException` |
| Invalid input state | `BadRequestException` |
```typescript
const block = await this.prisma.descriptionBlock.findUnique({ where: { id } });
if (!block) throw new NotFoundException(`Block ${id} not found`);
if (block.teamId !== teamId) throw new ForbiddenException();
```
Let NestJS handle unhandled exceptions. The default exception filter returns structured error responses with the correct HTTP status codes.