Files
youtube-studio-flow/documentation/YouTube Studio Flow/05 - Development/03 - Recipes.md
T

6.3 KiB

Recipes

Step-by-step instructions for common development tasks. For module structure conventions and shared service patterns, see the 02 - Backend architecture reference.


Recipe: Add a New Backend Module

  1. Create the module directory: backend/src/modules/my-feature/

  2. Create my-feature.module.ts:

import { Module } from '@nestjs/common';
import { PrismaModule } from '../../shared/prisma/prisma.module';
import { AuditModule } from '../../shared/audit/audit.module';
import { MyFeatureController } from './my-feature.controller';
import { MyFeatureService } from './my-feature.service';

@Module({
  imports: [PrismaModule, AuditModule],
  controllers: [MyFeatureController],
  providers: [MyFeatureService],
})
export class MyFeatureModule {}
  1. Create my-feature.controller.ts with the required decorators:
import { Controller, UseGuards, Request } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@ApiTags('my-feature')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('my-feature')
export class MyFeatureController {
  constructor(private readonly myFeatureService: MyFeatureService) {}
}
  1. Create my-feature.service.ts with PrismaService injection. Always scope DB queries to req.user.teamId:
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../shared/prisma/prisma.service';

@Injectable()
export class MyFeatureService {
  constructor(private readonly prisma: PrismaService) {}
}
  1. Register the module in AppModule imports array at backend/src/app.module.ts

  2. If the worker also needs access to this module's services (e.g. for use in a queue processor), register it in WorkerModule at backend/src/worker.module.ts as well

Audit logging: If the module performs user-facing mutations (create/update/delete), it must import AuditModule and inject AuditService. Call AuditService.log(actorId, entityType, entityId, action, before, after) in every mutation. See 04 - Backend Architecture Patterns for the full audit pattern.


Recipe: Add a New Lint Rule

  1. Create backend/src/modules/linting/rules/my-rule.rule.ts:
import { LintRule, LintIssue } from './base.rule';
import { LintSeverity } from '@prisma/client';

export class MyRule implements LintRule {
  code = 'MY_RULE_CODE';
  severity = LintSeverity.WARNING;

  check(video: any): LintIssue | null {
    if (/* condition */) {
      return {
        targetField: 'title',
        message: 'Describe the problem clearly',
        fixSuggestion: 'Explain how to fix it',
      };
    }
    return null;
  }
}
  1. Open backend/src/modules/linting/linting.service.ts, import the new rule class, and add an instance to the rules[] array:
private readonly rules: LintRule[] = [
  new ExistingRule(),
  new MyRule(), // add here
];
  1. The rule runs automatically on all subsequent lint jobs. It can be disabled per-team in the Settings page (lint rule disabling is stored on the Team model). See 03 - Metadata Linting for the full lint system overview.

Recipe: Add a New API Endpoint to the Frontend

  1. If the endpoint returns a new response shape, add a TypeScript interface to frontend/src/lib/api.ts:
export interface MyResponse {
  id: string;
  name: string;
  // ...
}
  1. Add the API function in the same file:
export const myNewAction = (id: string, data: MyData): Promise<MyResponse> =>
  apiClient.post<MyResponse>(`/my-feature/${id}/action`, data).then(r => r.data);
  1. Use it in a component with TanStack Query:
const qc = useQueryClient(); // must be called at component level, not inside a callback

const mut = useMutation({
  mutationFn: () => myNewAction(id, data),
  onSuccess: () => {
    qc.invalidateQueries({ queryKey: ['affected-key'] });
  },
});

See 03 - Component Patterns for TanStack Query conventions, including placeholderData usage and multi-key invalidation.


Recipe: Add a New Sidebar Navigation Item

  1. Open frontend/src/components/shared/Sidebar.tsx

  2. Import the icon from lucide-react:

import { MyIcon } from 'lucide-react';
  1. Add an entry to the navItems array:
{ label: 'My Page', href: '/my-page', icon: MyIcon, group: 'WORKSPACE' },

Available groups and their current members:

Group Used for
WORKSPACE Overview, Videos, Calendar
OPERATIONS Bulk Jobs, Import/Export
PEOPLE Collaborators, Teams
LOGGING Audit Log, Quota
  1. Create the corresponding route at frontend/src/app/(dashboard)/my-page/page.tsx

Recipe: Add a New Prisma Field

  1. Add the field to the appropriate model in backend/prisma/schema.prisma

  2. Stop the backend (and worker if running)

  3. Create and apply the migration:

npx prisma migrate dev --name add-my-field
  1. Regenerate the Prisma client:
npx prisma generate
  1. Restart the backend and worker

Never remove enum values from the Prisma schema. PostgreSQL enum removal requires a raw SQL migration and risks data loss if any rows reference the removed value. Mark deprecated values in the UI instead. See 04 - Gotchas for detail.


Recipe: Add a New System Token (e.g. {video.myField})

System tokens are built-in substitution tokens like {video.title} or {collab.youtube} that are handled by dedicated resolvers rather than team variable lookup.

  1. Add the new token definition to SYSTEM_VARIABLES[] in: backend/src/shared/system-variables/system-variables.registry.ts

  2. Add the token string to the SYSTEM_VARIABLE_TOKENS Set in the same file:

export const SYSTEM_VARIABLE_TOKENS = new Set([
  // existing tokens...
  '{video.myField}',
]);
  1. Implement the resolution logic in RenderEngineService (backend/src/shared/render-engine/render-engine.service.ts) in the appropriate resolver method

Critical: Without step 2, resolveVariables() will treat the token as a team variable key, find nothing, and silently produce an empty string in the rendered description. This is a common source of invisible rendering bugs. See 07 - Render Engine for the full token resolution pipeline.