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
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:
- SupabaseAuthGuard — validates the JWT from Supabase Auth.
- ChapterGuard — verifies the
x-chapter-idheader and membership in that chapter. - 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
userstable.
3. Adding a new module
Example: adding a polls module.
-
Domain layer
- Create
src/domain/entities/poll.entity.tswith a TypeScript interface representing the table. - Create
src/domain/repositories/poll.repository.tsdefining an interface (e.g.IPollRepository).
- Create
-
Infrastructure layer
- Implement
SupabasePollRepositoryinsrc/infrastructure/supabase/repositories/poll.repository.ts. - Use the shared
SupabaseClientprovider to query thepollstable.
- Implement
-
Application layer
- Add
PollServiceinsrc/application/services/poll.service.ts. - Inject
IPollRepositoryand implement use-cases:createPoll,vote,closePoll,listPollsForChannel.
- Add
-
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.
- Add DTOs in
-
Module wiring
- Create
PollModuleinsrc/interface/modules/poll.module.ts, providing controller, service, and repository implementation. - Import
PollModuleintoAppModule.
- Create
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.
/healthendpoint 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.