Contracts and Types
This page explains how this codebase separates application DTOs, shared contracts, domain types, and port types, and where each one is used.
Quick Definitions
- Application DTOs: module-local input/output/query types for use-cases and application ports.
- Shared contracts: transport-level request/response schemas and types shared across package boundaries.
- Domain types: business-language concepts owned by a module.
- Port types: application-layer dependency contracts implemented by adapters.
1) DTOs (Application Layer)
In this repo, DTOs live in apps/api/src/modules/*/application/dtos/*.
Examples:
users/application/dtos/user.ts:RegisterUserCommand,RegisterUserResultUpdateProfileCommand,UserProfileSetUserInterestsCommand,UserInterests
auth/application/dtos/auth.ts:AuthenticatedIdentity,ResolveInternalUserIdCommand
webhooks/clerk-webhooks/application/dtos/clerk-webhook.ts:ClerkWebhookCommand,ClerkWebhookResult
Where DTOs are used:
- Use-cases: command/query/result inputs and outputs.
- Example:
register-user-by-identity-provider-id.tsconsumesRegisterUserCommandand returnsRegisterUserResult. - Example:
resolve-internal-user-id.tsconsumesResolveInternalUserIdCommand.
- Example:
- Application ports: method signatures depend on DTOs.
- Example:
users/application/ports/user-repo.port.tsusesUserProfile,UserInterests, and command/query DTOs.
- Example:
- Adapters inside same module: map HTTP/persistence/webhook data to DTOs.
- Example: users HTTP handlers import DTOs for typed calls into use-cases.
- Example: persistence repo implementation imports DTOs for typed return values.
- Cross-module at application boundary: one module can consume another module’s DTO when calling its use-case.
- Example: auth use-case imports
RegisterUserResultfrom users DTOs.
- Example: auth use-case imports
Rules for DTOs:
- keep in
modules/<feature>/application/dtos - represent application intent (command/query/result), not HTTP transport or DB table shape
- safe to evolve with module internals as long as call sites are updated
2) Shared Contracts (packages/contracts)
Shared contracts live in packages/contracts/* and are imported as contracts/*.
Current usage in API:
contracts/userused in HTTP adapters:auth/adapters/http/routes.tsusers/adapters/http/routes.tsusers/adapters/http/handlers.ts
contracts/systemused in HTTP adapters:auth/adapters/http/routes.tsusers/adapters/http/routes.tswebhooks/clerk-webhooks/adapters/http/routes.ts
What shared contracts contain:
- TypeBox schemas (runtime validation / OpenAPI-compatible route schemas), for example:
RegisterUserRequestSchema,RegisterUserResponseSchemaBadRequestResponseSchema,UnauthorizedResponseSchema
- Derived TS types from schemas, for example:
RegisterUserRequest,RegisterUserResponseInternalServerErrorResponse
Rules for shared contracts:
- use them at package/module boundaries, especially HTTP request/response schemas
- keep them backward-compatible when consumed by multiple places
- do not move module-internal application intent here unless it is truly a stable shared boundary
3) Domain Types
Domain types model business concepts and invariants inside a module.
Example:
export type AuthenticatedIdentity = {
userId: string;
identityProviderUserId: string;
};
Rules:
- keep in
modules/<feature>/domain - name with business language
- do not include HTTP/DB/framework concerns
4) Port Types
Port types define what the application needs, not how it is implemented.
Example from auth:
export type IdentityProviderPort = {
authenticate: (request: AuthRequestContext) => Promise<AuthenticatedIdentity | null>;
};
Rules:
- keep in
modules/<feature>/application/ports - express required behavior as small function contracts
- adapters implement ports (Clerk, DB, external APIs, etc.)
How They Work Together
- Shared contract defines HTTP boundary shapes (schema + inferred type) in
packages/contracts. - HTTP adapter validates/parses request by contract and maps to DTOs.
- Use-case works with DTOs and domain concepts, calling ports as needed.
- Adapters (repo/provider/webhook) implement ports and return DTO/domain-safe data.
- Domain logic stays independent from transport concerns.
Decision Guide
Ask these in order:
- Is this request/response validation or public boundary schema? -> Shared contract (
packages/contracts) - Is this use-case input/output/query inside application layer? -> DTO
- Is this core business concept/invariant? -> Domain type
- Is this dependency behavior required by a use-case? -> Port type
If a shape starts as DTO and later becomes a stable external boundary, promote it deliberately to shared contracts.