Appearance
🔄 Synced from
castyou-backend/docs/PATTERNS.md— edit it there, not here.
CastYou Backend — Patterns & Conventions
GraphQL Resolver Pattern
Resolvers are thin. They handle:
- Auth guards (
requireAuth,requireRole) - Input extraction from GraphQL args
- Delegation to the service layer
- Return of the result
They do not contain:
- Business logic
- Direct database calls (except simple reads that have no business logic)
- External API calls
ts
// Good
updateTalentProfile: async (_, { input }, ctx) => {
requireAuth(ctx);
return talentService.updateProfile(ctx.user.sub, input);
},
// Bad — logic leaking into resolver
updateTalentProfile: async (_, { input }, ctx) => {
requireAuth(ctx);
const existing = await ctx.prisma.talentProfile.findUnique(...);
if (!existing) await ctx.prisma.talentProfile.create(...);
else await ctx.prisma.talentProfile.update(...);
// ... more logic
},Service Layer Pattern
Services are plain functions (not classes). Each service file owns one domain.
ts
// src/services/talent/index.ts
export async function updateProfile(userId: string, input: UpdateInput): Promise<TalentProfile> {
// validation, business rules, DB call
}Services receive typed inputs, never raw GraphQL args. They throw AppError (from middleware/errorHandler.ts) for expected failures.
Error Handling
- GraphQL errors (client-facing): throw
GraphQLErrorwith a meaningfulextensions.code - HTTP errors (REST/health): throw
AppError(statusCode, message) - Unexpected errors: let them bubble to
errorHandlermiddleware which logs and returns 500
ts
// Client-facing auth error
throw new GraphQLError('You must be logged in.', {
extensions: { code: 'UNAUTHENTICATED' },
});
// Business rule violation
throw new GraphQLError('Job is no longer accepting applications.', {
extensions: { code: 'BAD_USER_INPUT' },
});Zod Validation
All external inputs (env vars, service inputs from REST endpoints) are validated with Zod. GraphQL input types provide schema-level validation; add Zod for business rule validation inside service functions.
ts
const createJobSchema = z.object({
title: z.string().min(3).max(120),
paymentType: z.enum(['FLAT_RATE', 'HOURLY', ...]),
});DataLoader Pattern (N+1 Prevention)
For fields that load related data (e.g., Job.producer), use DataLoader. Create loaders in createContext and attach to ctx.loaders.
ts
// ctx.loaders.producerById.load(producerId)Environment Config
All env access goes through src/config/index.ts. Never read process.env directly in application code. The config module validates all required vars at startup using Zod and exits with a clear error if any are missing.
Testing
- Unit tests: service functions with mocked DB
- Integration tests: use a real test PostgreSQL instance (not mocked)
- Test files live in
__tests__/next to the code they test - Use
vitestfor all tests
Naming Conventions
- Files:
camelCase.ts - Types/interfaces:
PascalCase - Functions/variables:
camelCase - GraphQL types:
PascalCase - Prisma models:
PascalCase - DB table names:
snake_case(via@@map) - Env vars:
SCREAMING_SNAKE_CASE