Backend
Conventions for apps/back and apps/scraper — layering, authorization, errors, types, logging
Read this before building or changing any API feature. Unlike the frontend, parts of the existing code don't follow these rules yet (see the plan) — new and edited code must; don't copy the legacy patterns.
Layering
- Module-per-domain:
controller/service/dto//module, plus*.mapper.tsfor the DB→API boundary. - Controllers never import
prisma; only services touch the DB. A controller method is ≤ ~10 lines: guard decorators, DTO in, service call, mapper out. Business rules (find-or-create, status transitions) live in the service. - The scraper app writes only
Scraper*/ScrapedProducttables; catalog writes (Product,Spec,Shop) stay inback'sProductsService. Job payload types belong in@marketplace/types, declared once for producer and consumer.
Authorization
The non-negotiable rule
Every mutation on a user-owned resource (deals, discussions, messages) verifies ownership/participation against @CurrentUser() in the service — one private assertion per service (assertCanModify(id, user)), admins bypass via role check inside the assertion, never by skipping it.
- Status transitions are guarded explicitly (e.g. publish only from
DRAFT/DECLINED). - Anything that touches external storage from a client-provided value (e.g. deleting an image by URL) checks both provenance (
isS3Url) and membership (the URL belongs to the resource being edited). - New protected routes:
@UseGuards(JwtAuthGuard)class-level,@Public()/@Roles(UserRole.ADMIN)per endpoint.
Errors
- Only Nest
HttpExceptionsubclasses cross the controller boundary:NotFoundException,ForbiddenException,BadRequestException,ConflictException. Neverthrow new Error(...)— it becomes a 500 and hides authz failures from monitoring. findById-style endpoints throw 404, they don't returnnullwith 200.- Anti-enumeration responses (password reset) keep the response constant but still log unexpected errors.
Types
- No
Promise<any>on new/edited methods. Pattern:Prisma.validatorpayload types (DealWithRelations = Prisma.DealGetPayload<typeof dealWithRelations>) as service return types, mapped to@marketplace/typesshapes in the mapper. When touching a legacy method, fix its signature as part of the change. @CurrentUser() user: RequestUser(fromcommon/types/), neveruser: any.- Status/condition parameters are typed with Prisma enums (
DealStatus,ScrapedProductStatus) — no magic strings, noas any.
DTOs & validation
- Every write endpoint has a DTO class; enum fields use
@IsEnum(...); query strings get query DTOs (@Type(() => Number),@Transformfor comma-separated ids) instead of long@Query('x')lists. - Global
ValidationPipe({ whitelist: true })is on; target state addsforbidNonWhitelisted: true— don't rely on silent stripping.
Data access
- Multi-writes that must be all-or-nothing go through
$transaction(or nestedcreateMany); no delete-then-loop-create. - Prisma ignores
undefinednatively — no...(x !== undefined && { x })spread chains. - List endpoints clamp
limit(max 50) via the shared pagination helper (pageArgs/buildMetaincommon/); never pass a raw query param totake. - Schema changes go through
prisma migrate dev(committed migration), neverdb:push, and money columns areDecimal, notFloat.
Config & secrets
- Read config through
ConfigService; never|| 'some-default'fallbacks for secrets — required vars are enforced by the Joi schema (when('NODE_ENV', { is: 'production', then: required() })). A missing prod secret must crash the boot, not degrade silently. - New env vars are added to
env.config.ts(Joi),.env.example, andDEPLOYMENT.mdin the same PR.
Logging
Nest Logger (private readonly logger = new Logger(X.name)), never console.*. Log ids, not emails (PII). logger.error(message, error.stack) for failures.
Tests
- New/changed behavior gets a test in the same PR. E2E:
test/*.e2e-spec.tswith supertest against a real Postgres (testcontainers); unit specs colocated*.spec.ts. Each e2e file creates its own users with unique emails; assert specific fields, no snapshots. - Authorization rules are exactly the thing e2e must encode: "user B PATCHes user A's deal → 403" style.
- Dependencies must be Jest/Vitest-loadable: prefer Node built-ins (
crypto.randomUUID()) over ESM-only micro-deps (an ESM-onlyuuidbroke the whole back test suite).
Verification gates
pnpm --filter back lint && pnpm --filter back exec tsc --noEmit
pnpm --filter back test && pnpm --filter back buildGreps that must be clean in files you touched:
grep -n "throw new Error(" <files>
grep -nE "Promise<any>|as any|user: any" <files>
grep -n "import { prisma }" <files> # services use PrismaService/DI, controllers use nothing
grep -n "console\." <files>