This document walks through the end-to-end path of a request: from HTTP through validation, use case, repository, and events. It clarifies where DTOs/schemas are used and where DomainError is thrown and handled.
Flow for POST /api/users/with-keycloak (or a protected POST /api/users that creates a user):
- Route —
api.routes.tsmounts the handler. For signup:POST /users/with-keycloakwithvalidate({ body: CreateKeycloakUserDto }). - Validate — Middleware runs Zod:
body(and optionallyquery,params). On success,req.bodyis replaced with the parsed value; on failure, aDomainErrorwith codeVALIDATION_FAILEDis passed tonext()and the error middleware returns 400 + details. - Controller — e.g.
UserController.create()orcreateWithKeycloak(). Receives validated body (typed asTCreateUserDto/TCreateKeycloakUserDto). Calls the use case:this.createUserUC.execute(req.body). - Use case —
CreateUserUseCase.execute(input). Uses only interfaces:IUserRepository,IEventPublisher. It may throwDomainError(e.g.EMAIL_IN_USE,USER_INACTIVE). On success it callsthis.users.create(newUser)thenthis.events.publish(USER_CREATED, payload). - Repository —
UserRepository(infra) implementsIUserRepository.create()callsprisma.user.create(...)and returns the created user. Prisma errors are mapped toDomainError(e.g. unique →EMAIL_IN_USE, not found →NOT_FOUND) in the repository or via a decorator/mapper. - Event bus — Use case publishes
USER_CREATED. Subscribers (e.g. inpackages/core/src/subscribers/userLogging.ts) are registered witheventBusfrom infra; they run when the event is published (e.g. log to console). - Response — Use case returns the created user; controller sends
res.status(201).json(user). Any error thrown from the use case or repository is passed tonext(err)and handled by the error middleware (e.g.DomainError→ status + code + details).
Flow for GET /api/users/:id (protected):
- Route — e.g.
GET /users/:idwithvalidate({ params: UserParamsDto }). - Validate —
params.idmust match the schema (e.g. UUID). Invalid params →DomainError→ error middleware. - Controller —
UserController.getById(req, res, next). Readsreq.params.id, callsthis.getUserByIdUC.execute(req.params.id). - Use case —
GetUserByIdUseCase.execute(id). Callsthis.users.findById(id). ReturnsUser | null; controller may map null to 404 (or use case throwsDomainErrorwithNOT_FOUND). - Repository —
UserRepository.findById(id)usesprisma.user.findUnique(). Maps Prisma errors toDomainErrorwhere applicable. - Response — Controller returns 200 + user or 404.
sequenceDiagram
participant Client
participant Route
participant Validate
participant Controller
participant UseCase
participant IRepo
participant Prisma
participant EventBus
Client->>Route: POST /users
Route->>Validate: body schema
Validate->>Controller: validated body
Controller->>UseCase: execute(dto)
UseCase->>IRepo: create(data)
IRepo->>Prisma: prisma.user.create()
Prisma-->>IRepo: user
IRepo-->>UseCase: user
UseCase->>EventBus: publish(USER_CREATED)
UseCase-->>Controller: user
Controller-->>Client: 201 + user
- Validation (edge): The
validatemiddleware uses Zod schemas (often the same as DTOs from@repo/shared). It validatesbody,query, and/orparams. After validation,req.body/req.query/req.paramshold the parsed values; TypeScript types (e.g.TCreateUserDto) describe these in controllers. - Use case input: Use cases receive already-validated data. They typically accept DTO types (e.g.
TCreateUserDto) or ids and params. They do not re-validate; validation is done once at the HTTP boundary. - Pagination/sort: List endpoints use DTOs or shared types for offset/cursor params and sort (e.g.
TUserOffsetPagination,TUserCursorPagination). Same idea: validate at route, pass typed params into use case.
- Validation middleware: Invalid body/query/params →
DomainError({ code: "VALIDATION_FAILED", ... })→next(err). - Use cases: Business rules (e.g. email already in use, user inactive) →
throw new DomainError({ code: "EMAIL_IN_USE", ... }). - Repositories / Prisma layer: Prisma errors are mapped to
DomainError(e.g. P2002 → conflict, P2025 → not found) inprisma-error-mapper.tsor repository code; then thrown so the controller’scatchor Express error middleware can handle them. - Error middleware: In
apps/api/src/infra/http/middlewares/error-handler.middleware.ts. IferrisDomainError, it responds with the appropriate status and body (code, message, details); otherwise it may send a generic 500.
This keeps a single, consistent error shape for the API and ensures that validation, business, and infrastructure failures all flow through the same handler.