ORM Integration
ORM Integration
Section titled “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.
Table of Contents
Section titled “Table of Contents”- Drizzle ORM Integration
- Prisma Integration
- TypeORM Integration
- Base Repository Pattern
- Transactions
- Best Practices
Drizzle ORM Integration
Section titled “Drizzle ORM Integration”Installation
Section titled “Installation”bun add drizzle-orm# SQLite (Bun native)bun add drizzle-orm @libsql/client# PostgreSQLbun add drizzle-orm postgres# MySQLbun add drizzle-orm mysql2Registering Drizzle with the DI Container
Section titled “Registering Drizzle with the DI Container”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 compilingregisterDrizzle(app, db);
app.include(ProductController);await app.compile();app.listen(3000);Injecting the Database with @InjectDB
Section titled “Injecting the Database with @InjectDB”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]; }}Custom Injection Token
Section titled “Custom Injection Token”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();}Defining a Schema
Section titled “Defining a Schema”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(),});Prisma Integration
Section titled “Prisma Integration”Installation
Section titled “Installation”bun add @prisma/clientbunx prisma initimport { 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 containerapp.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 }); }}TypeORM Integration
Section titled “TypeORM Integration”Installation
Section titled “Installation”bun add typeorm reflect-metadataimport { 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);Base Repository Pattern
Section titled “Base Repository Pattern”For a cleaner architecture, create repository classes and inject them via the standard DI container:
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; }}@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; }}Transactions
Section titled “Transactions”Drizzle Transactions
Section titled “Drizzle Transactions”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 }; });}Best Practices
Section titled “Best Practices”1. Register the DB before compiling
Section titled “1. Register the DB before compiling”// ✓ Correct orderregisterDrizzle(app, db);app.include(ProductController);await app.compile(); // <-- always awaitapp.listen(3000);
// ✗ Wrong - registering after compileawait app.compile();registerDrizzle(app, db); // too late2. Use a single DB instance (singleton)
Section titled “2. Use a single DB instance (singleton)”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.
Next Steps
Section titled “Next Steps”- Read the Pagination Guide for cursor and offset pagination helpers
- Learn about Dependency Injection
- Explore Caching to reduce database load
- Check the API Reference