Skip to content

Backend Structure

The backend uses NestJS modules grouped by business capability. The structure is deliberately pragmatic: code that changes together stays together, while cross-cutting infrastructure has an explicit home.

Directory Map

apps/backend/src/
├── app.module.ts          # Application composition root
├── core/                  # Health, application metadata, global interceptors
├── platform/              # Configuration, import, encryption, observability
├── shared/                # Feature-independent filters and small utilities
├── auth/                  # Authentication, clients, users, tenants, roles
├── crypto/                # Key and cryptographic operations
├── database/              # TypeORM setup and migrations
├── issuer/                # Issuer configuration, issuance, status lists
├── registrar/             # Registrar configuration and schema metadata
├── session/               # Session lifecycle, events, and session logging
├── storage/               # File persistence
├── trust/                 # Trust lists, federation, certificate validation
├── verifier/              # Presentation and credential verification
├── webhook/               # Outbound webhooks and URL policy
└── audit-log/             # Administrative audit log

Placement Rules

Use the narrowest owner that describes the code:

  1. Put protocol and business behavior in its feature directory. For example, status-list behavior belongs in issuer/status-list/, not in shared/.
  2. Put application-wide technical capabilities in platform/. Configuration loading, data-at-rest encryption, and logging setup are platform concerns.
  3. Put code in shared/ only when it is feature-independent, stateless or narrowly scoped, and safe to import from any feature. shared/ must not import from application features.
  4. Give every injectable provider one owning module. Other features import that module and use its exports instead of registering the provider again.
  5. Keep controllers, DTOs, entities, validation schemas, and services close to their feature. Split a module when its providers form a distinct capability with a clear public API.

Module Dependencies

AppModule is the composition root. It initializes global framework modules and imports the top-level application modules.

Within the application:

  • import another NestJS module when using its exported providers;
  • do not copy a foreign provider into a module's providers array;
  • avoid forwardRef() unless two modules have a real runtime cycle that cannot first be removed by changing ownership or extracting a smaller service;
  • keep feature imports out of shared/;
  • export only the providers or submodules that consumers actually require.

The boundary test in apps/backend/src/platform/module-boundaries.spec.ts protects the shared/ dependency rule and prevents old catch-all directories from returning.

Feature Module Shape

A feature does not need every directory below, but should use consistent names:

feature/
├── feature.module.ts
├── feature.controller.ts
├── feature.service.ts
├── feature.service.spec.ts
├── dto/
├── entities/
└── feature-validation.schema.ts

Small, feature-specific helpers can stay next to the service that uses them. Extract a service when the behavior has its own dependencies, lifecycle, or focused tests. NonceService and MetadataFetchService are examples of this boundary.

Adding or Moving Backend Code

Before opening a pull request:

  • identify the feature or platform capability that owns the behavior;
  • import the owning module rather than re-registering its provider;
  • add focused unit tests beside extracted services;
  • update imports, E2E utilities, generation scripts, and documentation paths;
  • run the backend format check, build, lint, and unit tests;
  • run relevant E2E tests when module wiring or protocol behavior changes.

From the repository root, the main backend checks are:

pnpm --filter @eudiplo/backend run format:check
pnpm --filter @eudiplo/backend run build
pnpm --filter @eudiplo/backend run lint
pnpm --filter @eudiplo/backend run test

When moving TypeORM entities, confirm that migration discovery and any data source imports still resolve. A source-code move alone should not create a database migration unless the persisted schema also changes.