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 { Injectable } from 'veloce-ts';
import { eq } from 'drizzle-orm';
import { products } from '../schema';
@Injectable()
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);
}
}

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.