Skip to main content

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, RegisterUserResult
    • UpdateProfileCommand, UserProfile
    • SetUserInterestsCommand, 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.ts consumes RegisterUserCommand and returns RegisterUserResult.
    • Example: resolve-internal-user-id.ts consumes ResolveInternalUserIdCommand.
  • Application ports: method signatures depend on DTOs.
    • Example: users/application/ports/user-repo.port.ts uses UserProfile, UserInterests, and command/query DTOs.
  • 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 RegisterUserResult from users DTOs.

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/user used in HTTP adapters:
    • auth/adapters/http/routes.ts
    • users/adapters/http/routes.ts
    • users/adapters/http/handlers.ts
  • contracts/system used in HTTP adapters:
    • auth/adapters/http/routes.ts
    • users/adapters/http/routes.ts
    • webhooks/clerk-webhooks/adapters/http/routes.ts

What shared contracts contain:

  • TypeBox schemas (runtime validation / OpenAPI-compatible route schemas), for example:
    • RegisterUserRequestSchema, RegisterUserResponseSchema
    • BadRequestResponseSchema, UnauthorizedResponseSchema
  • Derived TS types from schemas, for example:
    • RegisterUserRequest, RegisterUserResponse
    • InternalServerErrorResponse

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

  1. Shared contract defines HTTP boundary shapes (schema + inferred type) in packages/contracts.
  2. HTTP adapter validates/parses request by contract and maps to DTOs.
  3. Use-case works with DTOs and domain concepts, calling ports as needed.
  4. Adapters (repo/provider/webhook) implement ports and return DTO/domain-safe data.
  5. Domain logic stays independent from transport concerns.

Decision Guide

Ask these in order:

  1. Is this request/response validation or public boundary schema? -> Shared contract (packages/contracts)
  2. Is this use-case input/output/query inside application layer? -> DTO
  3. Is this core business concept/invariant? -> Domain type
  4. 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.