KwadMarket Docs
Conventions

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.ts for 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*/ScrapedProduct tables; catalog writes (Product, Spec, Shop) stay in back's ProductsService. 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 HttpException subclasses cross the controller boundary: NotFoundException, ForbiddenException, BadRequestException, ConflictException. Never throw new Error(...) — it becomes a 500 and hides authz failures from monitoring.
  • findById-style endpoints throw 404, they don't return null with 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.validator payload types (DealWithRelations = Prisma.DealGetPayload<typeof dealWithRelations>) as service return types, mapped to @marketplace/types shapes in the mapper. When touching a legacy method, fix its signature as part of the change.
  • @CurrentUser() user: RequestUser (from common/types/), never user: any.
  • Status/condition parameters are typed with Prisma enums (DealStatus, ScrapedProductStatus) — no magic strings, no as any.

DTOs & validation

  • Every write endpoint has a DTO class; enum fields use @IsEnum(...); query strings get query DTOs (@Type(() => Number), @Transform for comma-separated ids) instead of long @Query('x') lists.
  • Global ValidationPipe({ whitelist: true }) is on; target state adds forbidNonWhitelisted: true — don't rely on silent stripping.

Data access

  • Multi-writes that must be all-or-nothing go through $transaction (or nested createMany); no delete-then-loop-create.
  • Prisma ignores undefined natively — no ...(x !== undefined && { x }) spread chains.
  • List endpoints clamp limit (max 50) via the shared pagination helper (pageArgs/buildMeta in common/); never pass a raw query param to take.
  • Schema changes go through prisma migrate dev (committed migration), never db:push, and money columns are Decimal, not Float.

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, and DEPLOYMENT.md in 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.ts with 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-only uuid broke the whole back test suite).

Verification gates

pnpm --filter back lint && pnpm --filter back exec tsc --noEmit
pnpm --filter back test && pnpm --filter back build

Greps 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>

On this page