Skip to content

ORM Integration

Veloce-TS provides a built-in ORM layer with first-class support for Drizzle ORM, Prisma, and TypeORM, plus a dependency injection bridge so you can inject your database anywhere in the application.


Terminal window
bun add drizzle-orm
# SQLite (Bun native)
bun add drizzle-orm @libsql/client
# PostgreSQL
bun add drizzle-orm postgres
# MySQL
bun add drizzle-orm mysql2

Use registerDrizzle to make your database instance available everywhere via dependency injection:

import { VeloceTS, registerDrizzle } from 'veloce-ts';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { Database } from 'bun:sqlite';
const sqlite = new Database('app.db');
const db = drizzle(sqlite);
const app = new VeloceTS({
title: 'My API',
version: '1.0.0',
});
// Register before compiling
registerDrizzle(app, db);
app.include(ProductController);
await app.compile();
app.listen(3000);
import { Controller, Get, Post, Body, Param, HttpCode, InjectDB } from 'veloce-ts';
import { eq } from 'drizzle-orm';
import { products } from './schema';
import { z } from 'zod';
const CreateProductSchema = z.object({
name: z.string().min(2),
price: z.number().positive(),
stock: z.number().int().min(0).default(0),
});
@Controller('/products')
class ProductController {
@Get('/')
async list(@InjectDB() db: ReturnType<typeof drizzle>) {
return await db.select().from(products);
}
@Get('/:id')
async getById(
@Param('id') id: string,
@InjectDB() db: ReturnType<typeof drizzle>
) {
const result = await db
.select()
.from(products)
.where(eq(products.id, parseInt(id)));
if (!result.length) {
throw new NotFoundException('Product not found');
}
return result[0];
}
@Post('/')
@HttpCode(201)
async create(
@Body(CreateProductSchema) data: z.infer<typeof CreateProductSchema>,
@InjectDB() db: ReturnType<typeof drizzle>
) {
const inserted = await db
.insert(products)
.values(data)
.returning();
return inserted[0];
}
}

If you need multiple databases (e.g. read replica + write primary), use custom tokens:

import { registerDrizzle, InjectDB, DB_TOKEN } from 'veloce-ts';
const WRITE_DB = Symbol('WRITE_DB');
const READ_DB = Symbol('READ_DB');
registerDrizzle(app, primaryDb, WRITE_DB);
registerDrizzle(app, replicaDb, READ_DB);
@Get('/')
async list(
@InjectDB(READ_DB) readDb: any,
) {
return await readDb.select().from(products);
}
@Post('/')
async create(
@Body(CreateProductSchema) data: any,
@InjectDB(WRITE_DB) writeDb: any,
) {
return await writeDb.insert(products).values(data).returning();
}
src/schema.ts
import { sqliteTable, integer, text, real } from 'drizzle-orm/sqlite-core';
export const products = sqliteTable('products', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
price: real('price').notNull(),
stock: integer('stock').notNull().default(0),
createdAt: text('created_at').default(new Date().toISOString()),
});
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name').notNull(),
passwordHash: text('password_hash').notNull(),
});

Terminal window
bun add @prisma/client
bunx prisma init
import { VeloceTS } from 'veloce-ts';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const DB_TOKEN = Symbol('PRISMA_DB');
const app = new VeloceTS();
// Register Prisma using the generic DI container
app.getContainer().registerFactory(DB_TOKEN, () => prisma, { scope: 'singleton' });
app.include(UserController);
await app.compile();
app.listen(3000);
import { Controller, Get, Post, Body, Param, Depends, HttpCode } from 'veloce-ts';
import { PrismaClient } from '@prisma/client';
const DB_TOKEN = Symbol('PRISMA_DB');
@Controller('/users')
class UserController {
@Get('/')
async list(@Depends(DB_TOKEN) db: PrismaClient) {
return await db.user.findMany();
}
@Post('/')
@HttpCode(201)
async create(
@Body(CreateUserSchema) data: any,
@Depends(DB_TOKEN) db: PrismaClient
) {
return await db.user.create({ data });
}
}

Terminal window
bun add typeorm reflect-metadata
import { VeloceTS } from 'veloce-ts';
import { DataSource } from 'typeorm';
import { User } from './entities/user.entity';
const dataSource = new DataSource({
type: 'sqlite',
database: 'app.db',
entities: [User],
synchronize: true,
});
await dataSource.initialize();
const app = new VeloceTS();
const DS_TOKEN = Symbol('DATA_SOURCE');
app.getContainer().registerFactory(DS_TOKEN, () => dataSource, { scope: 'singleton' });
app.include(UserController);
await app.compile();
app.listen(3000);

For a cleaner architecture, create repository classes and inject them via the standard DI container:

src/repositories/product.repository.ts
import { eq } from 'drizzle-orm';
import { products } from '../schema';
class ProductRepository {
constructor(
@InjectDB() private db: ReturnType<typeof drizzle>
) {}
async findAll() {
return await this.db.select().from(products);
}
async findById(id: number) {
const result = await this.db
.select()
.from(products)
.where(eq(products.id, id));
return result[0] ?? null;
}
async create(data: typeof products.$inferInsert) {
const inserted = await this.db
.insert(products)
.values(data)
.returning();
return inserted[0];
}
async update(id: number, data: Partial<typeof products.$inferInsert>) {
const updated = await this.db
.update(products)
.set(data)
.where(eq(products.id, id))
.returning();
return updated[0] ?? null;
}
async delete(id: number) {
const deleted = await this.db
.delete(products)
.where(eq(products.id, id))
.returning();
return deleted.length > 0;
}
}
src/controllers/product.controller.ts
@Controller('/products')
class ProductController {
@Get('/')
async list(@Depends(ProductRepository) repo: ProductRepository) {
return await repo.findAll();
}
@Get('/:id')
async getById(
@Param('id') id: string,
@Depends(ProductRepository) repo: ProductRepository
) {
const product = await repo.findById(parseInt(id));
if (!product) throw new NotFoundException('Product not found');
return product;
}
@Post('/')
@HttpCode(201)
async create(
@Body(CreateProductSchema) data: any,
@Depends(ProductRepository) repo: ProductRepository
) {
return await repo.create(data);
}
}

Custom repositories and NotImplementedError

Section titled “Custom repositories and NotImplementedError”

The three bundled adapters (Drizzle, Prisma, TypeORM) all provide native overrides, so this only affects repositories you write yourself by extending BaseRepository directly.

If you do, override the three methods with real queries:

import { BaseRepository, NotImplementedError } from 'veloce-ts';
class ProductRepository extends BaseRepository<Product, number> {
// ...abstract members: create, findById, findOne, findMany, update, delete
async count(where?: FilterOptions): Promise<number> {
const [row] = await this.db
.select({ value: sql<number>`count(*)` })
.from(products)
.where(this.buildWhereClause(where));
return Number(row.value);
}
async deleteMany(where: FilterOptions): Promise<number> {
const result = await this.db.delete(products).where(this.buildWhereClause(where));
return result.rowCount ?? 0;
}
}
import { InjectDB } from 'veloce-ts';
@Post('/transfer')
async transfer(
@Body(TransferSchema) data: any,
@InjectDB() db: ReturnType<typeof drizzle>
) {
return await db.transaction(async (tx) => {
// Deduct from sender
await tx
.update(accounts)
.set({ balance: sql`balance - ${data.amount}` })
.where(eq(accounts.id, data.fromId));
// Add to receiver
await tx
.update(accounts)
.set({ balance: sql`balance + ${data.amount}` })
.where(eq(accounts.id, data.toId));
return { success: true, amount: data.amount };
});
}

// ✓ Correct order
registerDrizzle(app, db);
app.include(ProductController);
await app.compile(); // <-- always await
app.listen(3000);
// ✗ Wrong - registering after compile
await app.compile();
registerDrizzle(app, db); // too late

registerDrizzle registers as a singleton by default. Don’t create new connections per request.

3. Keep business logic in repositories or services

Section titled “3. Keep business logic in repositories or services”
// ✓ Good - thin controller, logic in repository
@Get('/:id')
async get(@Param('id') id: string, @Depends(ProductRepo) repo: ProductRepo) {
return await repo.findById(parseInt(id));
}
// ✗ Bad - SQL directly in controller
@Get('/:id')
async get(@Param('id') id: string, @InjectDB() db: any) {
return await db.select().from(products).where(eq(products.id, parseInt(id)));
}

4. Validate input before hitting the database

Section titled “4. Validate input before hitting the database”

Always use @Body(Schema) to validate input before any database operation. Veloce-TS will return a 422 automatically if validation fails.