Docs/Guide

API Architecture

This guide explains the NestJS architecture used by the Frapp API and how to add new modules safely.

1. Layered architecture

The API in apps/api follows a strict layered structure:

  • Interface layer: controllers, DTOs, guards, interceptors, exception filters
  • Application layer: services (use-cases, orchestration)
  • Infrastructure layer: Supabase repositories, external adapters (Stripe, Expo Push, Storage)
  • Domain layer: entities, repository interfaces, shared business rules
src/
  main.ts
  app.module.ts

  interface/
    controllers/
    dtos/
    guards/
    interceptors/
    filters/

  application/
    services/

  infrastructure/
    supabase/
      repositories/
    billing/
    notifications/
    storage/

  domain/
    entities/
    repositories/
    adapters/
    constants/permissions.ts
Info

Controllers only handle HTTP concerns (routing, status codes, DTOs). They never talk to Supabase directly — they call application services instead.

2. Guards and interceptors

Every protected endpoint runs through a consistent guard chain:

  1. SupabaseAuthGuard — validates the JWT from Supabase Auth.
  2. ChapterGuard — verifies the x-chapter-id header and membership in that chapter.
  3. PermissionsGuard — checks @RequirePermissions() metadata against the user's roles.

Interceptors:

  • RequestIdInterceptor — attaches/propagates x-request-id.
  • LoggingInterceptor — structured JSON logging with latency and status code.
  • (Future) AuthSyncInterceptor — syncs Supabase Auth metadata into our users table.

3. Adding a new module

Example: adding a polls module.

  1. Domain layer

    • Create src/domain/entities/poll.entity.ts with a TypeScript interface representing the table.
    • Create src/domain/repositories/poll.repository.ts defining an interface (e.g. IPollRepository).
  2. Infrastructure layer

    • Implement SupabasePollRepository in src/infrastructure/supabase/repositories/poll.repository.ts.
    • Use the shared SupabaseClient provider to query the polls table.
  3. Application layer

    • Add PollService in src/application/services/poll.service.ts.
    • Inject IPollRepository and implement use-cases: createPoll, vote, closePoll, listPollsForChannel.
  4. Interface layer

    • Add DTOs in src/interface/dtos/poll.dto.ts.
    • Add a controller in src/interface/controllers/poll.controller.ts.
    • Decorate endpoints with @UseGuards(SupabaseAuthGuard, ChapterGuard, PermissionsGuard) and @RequirePermissions("polls:manage") as needed.
  5. Module wiring

    • Create PollModule in src/interface/modules/poll.module.ts, providing controller, service, and repository implementation.
    • Import PollModule into AppModule.
Tip

Always start new features by updating the specs (spec/product.md, spec/behavior.md, spec/architecture.md). The API implementation should follow, not lead, the spec.

4. Error handling

We use a global AllExceptionsFilter to normalize error responses:

  • Shape: { statusCode, error, message, requestId }
  • All unhandled exceptions are logged with the request ID.
  • 5xx errors are reported to Sentry with full context.

When adding new modules:

  • Throw Nest's HttpException (e.g. BadRequestException, ForbiddenException) for expected errors.
  • Let unexpected errors bubble up to the exception filter so they're logged and reported.

5. Observability hooks

The API surface is instrumented for observability:

  • Structured logging with request ID, user ID, chapter ID, method, path, status, latency.
  • /health endpoint used by load balancers and uptime checks.
  • Sentry integration in the Nest bootstrap.

When you add new modules:

  • Reuse existing logging patterns in services and repositories.
  • Do not add ad‑hoc console.log — use the injected logger or rely on the interceptors.