Drizzle
@miiajs/drizzle integrates Drizzle ORM with MiiaJS, providing connection management, retry logic, and DI-based access to the database.
Installation
Install the core package and a database driver. MiiaJS is bun-native - swap bun add for npm install, pnpm add, or yarn add if you use a different package manager.
bun add @miiajs/drizzle drizzle-orm postgres
bun add @miiajs/drizzle drizzle-orm mysql2
bun add @miiajs/drizzle drizzle-orm better-sqlite3
Setup
Configure the module
import { Module } from '@miiajs/core'
import { DrizzleModule } from '@miiajs/drizzle'
@Module({
imports: [
DrizzleModule.configure({
dialect: 'postgres',
connection: { url: 'postgres://localhost:5432/mydb' },
}),
],
})
class AppModule {}
With ConfigService (from @miiajs/config):
import { ConfigService } from '@miiajs/config'
DrizzleModule.configure((resolve) => {
const config = resolve(ConfigService)
return {
dialect: 'postgres',
connection: {
url: config.getOrThrow('DATABASE_URL'),
retry: { attempts: 10, delay: 10_000 },
},
}
})
Configuration options
interface DrizzleModuleOptions {
dialect: 'postgres' | 'mysql' | 'sqlite'
connection: {
url: string
retry?: {
attempts?: number // Default: 10
delay?: number // Default: 10_000 ms
}
}
schema?: Record<string, unknown>
casing?: 'snake_case' | 'camelCase'
}
Define schemas
Use standard Drizzle table definitions:
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
role: varchar('role', { length: 50 }).notNull().default('user'),
createdAt: timestamp('created_at').default(sql`now()`),
})
Register schemas
All schemas live in DrizzleModule.configure({ schema }). Drizzle is schema-first - tables are imported directly into services as values, so there is no per-feature DI registration. Use a barrel file (src/db.schema.ts) that re-exports every table for a single typeof schema reference:
// src/db.schema.ts
export * from './user/user.schema.js'
export * from './post/post.schema.js'
// src/app.module.ts
import * as schema from './db.schema.js'
@Module({
imports: [
DrizzleModule.configure({
dialect: 'postgres',
connection: { url: DATABASE_URL },
schema,
}),
],
})
class AppModule {}
Use in services
Define a typed DI token once with drizzleDb<TDb>() and inject it with the framework's standard inject() helper:
// src/db.ts
import { drizzleDb } from '@miiajs/drizzle'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import type * as schema from './db.schema.js'
export const db = drizzleDb<PostgresJsDatabase<typeof schema>>()
// src/user/user.service.ts
import { Injectable, inject } from '@miiajs/core'
import { eq } from 'drizzle-orm'
import { db } from '../db.js'
import { users } from './user.schema.js'
@Injectable()
class UserService {
private db = inject(db)
async findAll() {
return this.db.select().from(users)
}
async findById(id: number) {
const [user] = await this.db
.select()
.from(users)
.where(eq(users.id, id))
return user ?? null
}
async create(data: { name: string; email: string }) {
const [user] = await this.db
.insert(users)
.values(data)
.returning()
return user
}
async update(id: number, data: Partial<{ name: string; email: string }>) {
const [user] = await this.db
.update(users)
.set(data)
.where(eq(users.id, id))
.returning()
return user
}
async delete(id: number) {
await this.db.delete(users).where(eq(users.id, id))
}
}
Type safety
drizzleDb<TDb>() accepts the concrete Drizzle database type as a generic parameter. The type flows through inject(db) so every call site gets full autocompletion for select, insert, update, delete, execute, and - when you pass a schema - db.query.* relational queries.
For MySQL and SQLite, swap the type accordingly: MySql2Database<typeof schema> from drizzle-orm/mysql2, or BetterSQLite3Database<typeof schema> from drizzle-orm/better-sqlite3.
db.ts is the single source of truth.drizzleDb() memoizes by name only - the generic is compile-time. Calling drizzleDb<TypeA>() in one file and drizzleDb<TypeB>() in another resolves to the same runtime DI token, but projects different TypeScript types at each site. TypeScript will not catch the mismatch. Always import the typed db from your single db.ts (or analytics.ts, etc) - do not re-call drizzleDb<...>() elsewhere.Relational queries
When schemas include relations, db.query.* is typed automatically through the typeof schema generic:
import { relations } from 'drizzle-orm'
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))
Make sure the relations live in the schema barrel so they are part of typeof schema. Then:
const usersWithPosts = await this.db.query.users.findMany({
with: { posts: true },
})
Migrations
@miiajs/drizzle handles the runtime connection, but it does not run migrations on startup - onInit() only connects and retries on transient failures. Schema changes are a build/deploy step handled by drizzle-kit.
Install it as a dev dependency:
bun add -D drizzle-kit
npm install -D drizzle-kit
pnpm add -D drizzle-kit
yarn add -D drizzle-kit
Create drizzle.config.ts at the project root:
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/db.schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})
Then generate and apply migrations:
# Generate a new SQL migration from your schema changes
npx drizzle-kit generate
# Apply pending migrations against the database
npx drizzle-kit migrate
Commit both the generated SQL files in drizzle/ and the drizzle/meta/ snapshots - the snapshot is the baseline for the next generate.
.env when running scripts, so drizzle-kit sees DATABASE_URL through process.env without extra setup. For Node.js, pass node --env-file=.env or import dotenv/config at the top of drizzle.config.ts.drizzle-kit uses dialect: 'postgresql' while DrizzleModule.configure() uses dialect: 'postgres'. That's a naming mismatch between drizzle-kit and drizzle-orm/postgres-js, not a MiiaJS convention. Use whichever the surrounding API expects.A complete working example - including drizzle.config.ts, db:generate / db:migrate scripts, and a committed initial migration - lives in examples/drizzle-app.
Multiple connections
Use named connections for multi-database setups. Each connection gets its own schema barrel and its own typed drizzleDb<TDb>(name) token:
// src/db.ts - tokens for both connections
import { drizzleDb } from '@miiajs/drizzle'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import type * as mainSchema from './db.schema.js'
import type * as analyticsSchema from './analytics/db.schema.js'
export const db = drizzleDb<PostgresJsDatabase<typeof mainSchema>>()
export const analytics = drizzleDb<PostgresJsDatabase<typeof analyticsSchema>>('analytics')
// Root module - one configure() per connection
import * as mainSchema from './db.schema.js'
import * as analyticsSchema from './analytics/db.schema.js'
@Module({
imports: [
DrizzleModule.configure({
dialect: 'postgres',
connection: { url: MAIN_DB },
schema: mainSchema,
}),
DrizzleModule.configure(
{
dialect: 'postgres',
connection: { url: ANALYTICS_DB },
schema: analyticsSchema,
},
'analytics',
),
UserModule,
AnalyticsModule,
],
})
class AppModule {}
Inject the connection you need:
@Injectable()
class AnalyticsService {
private db = inject(analytics)
async getEvents() {
return this.db.select().from(events)
}
}
Supported databases
| Dialect | Driver | Version |
|---|---|---|
postgres | postgres | >= 3.0.0 |
mysql | mysql2 | >= 3.0.0 |
sqlite | better-sqlite3 | >= 11.0.0 |
All drivers are optional peer dependencies - install only what you need.
Connection lifecycle
- onInit - establishes connection with automatic retry on transient errors (
ECONNREFUSED,ECONNRESET,ETIMEDOUT,ENOTFOUND,EAI_AGAIN) - onDestroy - closes the connection gracefully
Testing
Override services at the DI level with TestApp.override() - handlers call your service, not the database directly, so you don't need to stub the Drizzle proxy API.
import { TestApp } from '@miiajs/core/testing'
import { AppModule } from './app.module.js'
import { UsersService } from './users/users.service.js'
const app = await TestApp.create(AppModule)
.override(UsersService, {
findAll: async () => [{ id: 1, name: 'Alice' }]
})
.compile()
const res = await app.request('GET', '/users')
expect(res.status).toBe(200)
expect(await res.json()).toEqual([{ id: 1, name: 'Alice' }])
await app.close()
For unit tests that exercise the service itself, override the database token instead:
import { drizzleDb } from '@miiajs/drizzle'
const app = await TestApp.create(AppModule)
.override(drizzleDb(), mockDb)
.compile()
Exports
import {
DrizzleModule,
drizzleDb,
type Dialect,
type DrizzleModuleOptions,
type DrizzleDbToken,
} from '@miiajs/drizzle'