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
-
Create the module directory:
backend/src/modules/my-feature/ -
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 {}
- Create
my-feature.controller.tswith 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) {}
}
- Create
my-feature.service.tswithPrismaServiceinjection. Always scope DB queries toreq.user.teamId:
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../shared/prisma/prisma.service';
@Injectable()
export class MyFeatureService {
constructor(private readonly prisma: PrismaService) {}
}
-
Register the module in
AppModuleimports array atbackend/src/app.module.ts -
If the worker also needs access to this module's services (e.g. for use in a queue processor), register it in
WorkerModuleatbackend/src/worker.module.tsas well
Audit logging: If the module performs user-facing mutations (create/update/delete), it must import
AuditModuleand injectAuditService. CallAuditService.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
- 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;
}
}
- Open
backend/src/modules/linting/linting.service.ts, import the new rule class, and add an instance to therules[]array:
private readonly rules: LintRule[] = [
new ExistingRule(),
new MyRule(), // add here
];
- 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
Teammodel). See 03 - Metadata Linting for the full lint system overview.
Recipe: Add a New API Endpoint to the Frontend
- 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;
// ...
}
- 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);
- 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
placeholderDatausage and multi-key invalidation.
Recipe: Add a New Sidebar Navigation Item
-
Open
frontend/src/components/shared/Sidebar.tsx -
Import the icon from
lucide-react:
import { MyIcon } from 'lucide-react';
- Add an entry to the
navItemsarray:
{ 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 |
- Create the corresponding route at
frontend/src/app/(dashboard)/my-page/page.tsx
Recipe: Add a New Prisma Field
-
Add the field to the appropriate model in
backend/prisma/schema.prisma -
Stop the backend (and worker if running)
-
Create and apply the migration:
npx prisma migrate dev --name add-my-field
- Regenerate the Prisma client:
npx prisma generate
- 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.
-
Add the new token definition to
SYSTEM_VARIABLES[]in:backend/src/shared/system-variables/system-variables.registry.ts -
Add the token string to the
SYSTEM_VARIABLE_TOKENSSet in the same file:
export const SYSTEM_VARIABLE_TOKENS = new Set([
// existing tokens...
'{video.myField}',
]);
- 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.