Skip to content
TALA
Esc
navigateopen⌘Jpreview
On this page

Modules

The 14 backend feature modules — where each one mounts, what it owns, and the route-ordering and scoping rules that are easy to get wrong.

The backend is 167 TypeScript files across 14 feature modules plus the app root. The global prefix is api, with /health explicitly excluded.

app.setGlobalPrefix("api", {
  exclude: [{ path: "health", method: RequestMethod.GET }],
});

The map

Module or controller Mount Routes Owns
AuthModule /api/auth 14 Registration, verification, password reset, login, OAuth, refresh rotation, logout
ProfileController /api/auth/user 3 Display name, avatar presign and commit — lives inside AuthModule
CollectionModule /api/collections 6 CRUD, pagination, per-user favourites
AssetsModule /api/assets 12 Read, update, delete, publish, download, versions, restore, asset logs
UploadModule /api/upload 3 S3 multipart orchestration
OrganizationModule /api/organizations 10 Organizations, members, invitations, and both organization guards
ProjectModule /api/projects 6 Project CRUD and board ordering
TaskModule /api/tasks + /api/projects/:projectId/tasks 8 Tasks, board moves, assignees
TaskCommentModule /api/tasks/:taskId/comments 4 Threaded comments with asset attachments
DashboardModule /api/dashboard 1 Org-scoped counts and recent task activity
ActivityLogsModule /api/timeline 4 HTTP audit log, the global interceptor, a retention cron
PlanModule /api/plan 1 Read-only subscription plans
SeederModule /api/seeder 1 Plan seeding, ADMIN only
AssetVersionModule 0 Entity and repository only; surfaced through AssetsModule
SharedModule 0 S3, CloudFront, paginator, token generator, query runner — @Global
AppController /health 1 Liveness, outside the api prefix

Two mounts that are not where you would look

TaskController is root-mounted. Its @Controller() decorator takes no prefix, so it declares both /api/tasks/* and /api/projects/:projectId/tasks from the same class. If you go looking for project tasks under src/project/, you will not find them there.

ProfileController is not its own module. It lives in src/auth/profile.controller.ts and mounts at /api/auth/user, so its routes sit a segment deeper than AuthController’s: /api/auth/user/profile, /api/auth/user/avatar-upload. Nothing collides with GET /api/auth/user — the paths are simply different.

Route declaration order matters

AssetController mounts GET logs above GET :id with a comment explaining why:

// NOTE: this MUST stay above @Get(':id'). Nest matches routes in declaration
// order, so a later literal path loses to an earlier parameterized one.
@Get("logs")
async getMyLogs(/* … */) {}

@Get(":id")
async getAssetById(/* … */) {}

Reorder those two and /api/assets/logs starts resolving as an asset whose ID is the literal string logs.

Transactional writes

Multi-step database writes go through QueryRunnerExec in src/shared/services/query-runner-exec.service.ts. Its commit and rollback always release the runner and are idempotent, so a double-rollback in an error path is safe.

Upload completion is the canonical example: asset, version, and log rows are persisted in a single transaction, so a failure never leaves a version row pointing at an asset that does not exist.

Custom exceptions

Throw ApplicationException from src/lib/exception/app.exception.ts for expected 4xx conditions. It is a bare Error subclass — services catch it and translate to the right Nest exception:

if (error instanceof ApplicationException)
  throw new NotFoundException(error.message);

this._logger.error((error as Error).message);
throw new InternalServerErrorException("Something went wrong");

Everything else reaches the global filter.

Scoping is not enforced by the compiler

Asset and collection reads and writes verify Asset → Collection → User == req.user and return 404 on mismatch, so a probe cannot distinguish “not yours” from “does not exist”.

Was this page helpful?