Skip to content

🔄 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 GraphQLError with a meaningful extensions.code
  • HTTP errors (REST/health): throw AppError(statusCode, message)
  • Unexpected errors: let them bubble to errorHandler middleware 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 vitest for 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