Skip to content

Repos

Repos are data access layers that handle database operations. They encapsulate queries and return results using the result pattern.

There are three types of repos:

All repos share the same patterns and concepts described below.

Transactional vs Non-Transactional Repos

There are two types of repos:

  • Transactional Repos – use this.trx, a single transaction connection shared across all repos in the use-case
  • Non-Transactional Repos – use this.db, the connection pool where each query runs independently

Transaction management is handled by Devux - you just use this.trx or this.db out of the box.

Domain repos and domain service repos are always transactional, as they operate within a use-case's transaction context.

Endpoint repos follow the transactional mode of their parent endpoint - transactional endpoints can only have transactional repos, and non-transactional endpoints can only have non-transactional repos.

Constraint Violation Handling

Database constraint violations (unique keys, foreign keys) are common. Instead of catching errors manually, repos let you map constraint names to error codes automatically.

Unique Key Violations

When an insert or update violates a unique constraint, map it to an error code:

typescript
@fullInjectable()
export class CreateCustomerRepo extends TransactionalRepo<...> {

    protected override getUniqueKeyViolationErrorMap() {
        return {
            'customers_email_unique': CustomersErrorCodes['CustomerEmailAlreadyExists'],
        };
    }
}

Now if the database throws a unique constraint error for customers_email_unique, the repo automatically returns { success: false, errorCode: 'customer_email_already_exists' } instead of throwing.

Foreign Key Violations

When an insert, update, or delete violates a foreign key constraint:

typescript
@fullInjectable()
export class CreateOrderRepo extends TransactionalRepo<...> {

    protected override getForeignKeyViolationErrorMap() {
        return {
            'orders_customer_id_fkey': OrdersErrorCodes['CustomerNotFound'],
        };
    }
}

This handles cases like inserting an order with a non-existent customer ID - instead of a raw database error, you get a clean { success: false, errorCode: 'customer_not_found' }.

Repo Structure

All repos follow the same file structure:

{repo-name}/
├── {repo-name}.repo.ts           # Implementation
├── {repo-name}.repo.zod.schemas.ts   # Input/output schemas
└── tests/
    └── {repo-name}.repo.test.ts  # Tests

Zod Schemas

Define input and output schemas in {repo}.repo.zod.schemas.ts:

typescript
// get-customer.repo.zod.schemas.ts
export const getCustomerRepoInputZodSchema = zodStrictPick(customersBaseZodSchema, {
    customerId: true,
});

export const getCustomerRepoOutputZodSchema = zodResult({
    data: zodStrictPick(customersBaseZodSchema, {
        name: true,
        phone: true,
        type: true,
    }),
    errorCodes: [CustomersErrorCodes['CustomerNotFound']],
});

Repo Implementation

The CLI generates the repo class structure in {repo}.repo.ts. You write the _execute method and optionally the constraint violation maps:

typescript
// get-customer.repo.ts
@fullInjectable()
export class GetCustomerRepo extends TransactionalRepo<GetCustomerRepoInput, GetCustomerRepoOutput> implements IGetCustomerRepo {

    // structure and types auto generated - you just write the implementation
    protected override async _execute(input: GetCustomerRepoInput): Promise<GetCustomerRepoOutput> {
        // example using kysely
        const customer = await this.trx
            .selectFrom('customers')
            .where('id', '=', input.customerId)
            .select(['name', 'phone', 'type'])
            .executeTakeFirst();

        if (customer === undefined) {
            return { success: false, errorCode: CustomersErrorCodes['CustomerNotFound'] };
        }

        return { success: true, data: customer };
    }

    // auto generated
    protected override getRepoInputZodSchema() {
        return getCustomerRepoInputZodSchema;
    }

    // auto generated
    protected override getRepoOutputZodSchema() {
        return getCustomerRepoOutputZodSchema;
    }
}

Performance Threshold

Repos have a default slow operation threshold (see Core Config). Override getDurationThresholdMillis() for operations that legitimately take longer:

typescript
protected override getDurationThresholdMillis(): number {
    return 200; // Custom threshold for this repo
}