Drizzle ORM
Learn how to setup Drizzle ORM with NuxtHub.
Setup
To enhance your Developer Experience with the database, we can create a useDrizzle()
server composable with few steps.
Install Drizzle
- Install the
drizzle-orm
package to your project:
pnpm add drizzle-orm
- Install
drizzle-kit
development dependency to your project:
pnpm add -D drizzle-kit
drizzle.config.ts
Add a drizzle.config.ts
file to your project:
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './server/database/schema.ts',
out: './server/database/migrations'
})
Database Schema
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
password: text('password').notNull(),
avatar: text('avatar').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
})
npm run db:generate
Let's add a db:generate
script to the package.json
:
{
"scripts": {
"db:generate": "drizzle-kit generate:sqlite"
}
}
When running the npm run db:generate
command, drizzle-kit
will generate the migrations based on server/database/schema.ts
and save them in the server/database/migrations
directory.
Migrations
We can create a server plugin to run the migrations in development automatically:
import { consola } from 'consola'
export default defineNitroPlugin(async () => {
if (!import.meta.dev) return
onHubReady(async () => {
const migrationsStorage = useStorage('root/server/database/migrations')
let migrationFiles = await migrationsStorage.getKeys()
migrationFiles = migrationFiles.filter(key => key.endsWith('.sql'))
const database = hubDatabase()
// Make sure to create the _hub_migrations table if it doesn't exist
await database.prepare('CREATE TABLE IF NOT EXISTS _hub_migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at INTEGER NOT NULL)').run()
// Get applied migrations from database
const hubMigrations = await database.prepare('SELECT * FROM _hub_migrations').all()
const appliedMigrations = (hubMigrations.results || []).map((row) => row.name)
const missingMigrations = migrationFiles.filter(file => !appliedMigrations.includes(file))
if (!missingMigrations.length) {
consola.success('Database up-to-date and ready')
return
}
// Apply missing migrations
const appliedMigrationsStmts = []
for (const file of missingMigrations) {
consola.info(`Applying database migrations from ${file}...`)
const migration = (await migrationsStorage.getItem<string>(file)) || ''
const statements = migration.split('--> statement-breakpoint')
for (let stmt of statements) {
await database.prepare(stmt.trim()).run()
}
appliedMigrationsStmts.push(database.prepare('INSERT INTO _hub_migrations (name, created_at) VALUES (?, ?)').bind(file, Date.now()))
}
await database.batch(appliedMigrationsStmts)
consola.success('Database migrations done')
})
})
_hub_migrations
table in your database to keep track of the applied migrations. It will also run the migrations automatically in development mode.useDrizzle()
Lastly, we can create a useDrizzle()
server composable to interact with the database:
import { drizzle } from 'drizzle-orm/d1'
export { sql, eq, and, or } from 'drizzle-orm'
import * as schema from '../database/schema'
export const tables = schema
export function useDrizzle() {
return drizzle(hubDatabase(), { schema })
}
export type User = typeof schema.users.$inferSelect
We are exporting the tables
object and the useDrizzle
function to be used in our API handlers without having to import them (Nuxt does it for us as long as it's exported from a server/utils/
file).
This allows you to conveniently reference your tables and interact directly with the Drizzle API.
User
type, which is inferred from the users
table. This is useful for type-checking the results of your queries.sql
, eq
, and
, and or
functions from drizzle-orm
to be used in our queries.Usage
Select
export default eventHandler(async () => {
const todos = await useDrizzle().select().from(tables.todos).all()
return todos
})
Insert
export default eventHandler(async (event) => {
const { title } = await readBody(event)
const todo = await useDrizzle().insert(tables.todos).values({
title,
createdAt: new Date()
}).returning().get()
return todo
})
Update
export default eventHandler(async (event) => {
const { id } = getRouterParams(event)
const { completed } = await readBody(event)
const todo = await useDrizzle().update(tables.todos).set({
completed
}).where(eq(tables.todos.id, Number(id))).returning().get()
return todo
})
Delete
export default eventHandler(async (event) => {
const { id } = getRouterParams(event)
const deletedTodo = await useDrizzle().delete(tables.todos).where(and(
eq(tables.todos.id, Number(id))
)).returning().get()
if (!deletedTodo) {
throw createError({
statusCode: 404,
message: 'Todo not found'
})
}
return deletedTodo
})