Database Adapters
Database Adapters
@websideproject/nuxt-auto-api supports multiple database engines through a unified adapter layer. Each adapter normalizes engine-specific behavior like transactions, batch operations, and mutation count parsing.
Supported Engines
| Engine | Package | atomic() | Returning | Native Batch |
|---|---|---|---|---|
better-sqlite3 | better-sqlite3 | transaction (BEGIN IMMEDIATE) | Yes | No |
postgres | pg / postgres | db.transaction() | Yes | No |
mysql | mysql2 | db.transaction() | No — rows are read back | No |
d1 | Cloudflare D1 | not a transaction (see below) | Yes | Yes |
turso | @libsql/client | db.transaction() | Yes | Yes |
planetscale | @planetscale/database (v1 — Drizzle 0.45 does not support v2) | db.transaction() | No — rows are read back | No |
On engines without RETURNING (MySQL, PlanetScale) the create/update routes insert or update, then select the
row by primary key, so responses are the same on every engine. Use insertReturning / updateReturning in
your own endpoints for the same behaviour.
Atomic writes on every engine — D1 included
D1 has no interactive transactions, so atomic(fn) cannot wrap an arbitrary async function there: on D1
it runs fn directly and adapter.supportsTransactions is false. What D1 does have is db.batch([...]),
a list of statements it runs as one transaction. The API's own multi-row writes are built for that — they do
their reads and checks first, then apply every statement together through atomicWrites():
| Write | All-or-nothing on |
|---|---|
Bulk create / update / delete (transactional: true) | every engine |
| Soft delete with its cascade (single and bulk) | every engine |
Batch restore / purge of a deletionId | every engine |
| M2M sync / add / remove / batch sync | every engine |
atomicWrites() runs the statements in a transaction where the engine has one and as one db.batch() on
D1. Use it in your own endpoints for the same guarantee — build each statement from the handle it is given,
and do not await it yourself:
import { atomicWrites } from '@websideproject/nuxt-auto-api/utils'
await atomicWrites([
db => db.update(orders).set({ status: 'paid' }).where(eq(orders.id, id)),
db => db.insert(payments).values({ orderId: id, amount }),
])
Because nothing runs until every statement is built, a statement cannot use an earlier one's result (an
inserted id, say) — generate ids up front. atomic() itself is re-entrant on the engines that have
transactions: an atomic() inside another joins the outer transaction.
D1 also binds at most 100 parameters per statement. The API splits the IN (…) lists it builds (linked
ids, included rows, cascade batches) to stay under it, and caps a $in / $nin filter at 50 values on D1
(500 elsewhere) — a larger one is a 400, not a database error.
Setup
Using initializeDatabase
Initialize the database with initializeDatabase() in a server plugin:
// server/plugins/database.ts
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { initializeDatabase } from '@websideproject/nuxt-auto-api/database'
import * as schema from '../database/schema'
export default defineNitroPlugin(() => {
const sqlite = new Database('sqlite.db')
const db = drizzle(sqlite, { schema })
initializeDatabase(db, 'better-sqlite3')
})
Postgres
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import { initializeDatabase } from '@websideproject/nuxt-auto-api/database'
import * as schema from '../database/schema'
export default defineNitroPlugin(() => {
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const db = drizzle(pool, { schema })
initializeDatabase(db, 'postgres')
})
MySQL
import { drizzle } from 'drizzle-orm/mysql2'
import mysql from 'mysql2/promise'
import { initializeDatabase } from '@websideproject/nuxt-auto-api/database'
import * as schema from '../database/schema'
export default defineNitroPlugin(() => {
const pool = mysql.createPool({ uri: process.env.DATABASE_URL })
const db = drizzle(pool, { schema, mode: 'default' })
initializeDatabase(db, 'mysql')
})
MySQL has no RETURNING: after an insert or update the row is read back by primary key, so responses match the
other engines.
PlanetScale
import { drizzle } from 'drizzle-orm/planetscale-serverless'
import { Client } from '@planetscale/database'
import { initializeDatabase } from '@websideproject/nuxt-auto-api/database'
import * as schema from '../database/schema'
export default defineNitroPlugin(() => {
const client = new Client({ url: process.env.DATABASE_URL })
const db = drizzle(client, { schema })
initializeDatabase(db, 'planetscale')
})
Cloudflare D1
import { drizzle } from 'drizzle-orm/d1'
import { initializeDatabase } from '@websideproject/nuxt-auto-api/database'
import * as schema from '../database/schema'
// The Worker's bindings exist per request (`event.context.cloudflare.env`), not while plugins start up —
// so initialize on the first request.
export default defineNitroPlugin((nitroApp) => {
let ready = false
nitroApp.hooks.hook('request', (event) => {
if (ready) return
const db = event.context.cloudflare?.env?.DB
if (!db) throw createError({ statusCode: 500, message: 'D1 binding "DB" not found' })
initializeDatabase(drizzle(db, { schema }), 'd1')
ready = true
})
})
D1 has no interactive transactions — see the callout above. Full walkthrough: Cloudflare D1.
Turso / LibSQL
import { drizzle } from 'drizzle-orm/libsql'
import { createClient } from '@libsql/client'
import { initializeDatabase } from '@websideproject/nuxt-auto-api/database'
import * as schema from '../database/schema'
export default defineNitroPlugin(() => {
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
})
const db = drizzle(client, { schema })
initializeDatabase(db, 'turso')
})
Configuration
The engine is the second argument of initializeDatabase(db, engine) — that is the only place it is set. The
autoApi.database module option is not read (it is kept only so older configs still type-check).
Using the Adapter Directly
The adapter is available in HandlerContext and from the getDatabaseAdapter() helper:
import { eq } from 'drizzle-orm'
import { getDatabaseAdapter } from '@websideproject/nuxt-auto-api/database'
import { insertReturning } from '@websideproject/nuxt-auto-api/utils'
// In a server handler
const adapter = getDatabaseAdapter()
// A transaction — except on D1, where `fn` runs directly (use atomicWrites() there)
const outcome = await adapter.atomic(async ({ tx }) => {
await tx.insert(users).values({ name: 'Alice' })
await tx.insert(posts).values({ title: 'Hello', userId: 1 })
return 'done'
})
// Insert and get the row back on every engine
const [created] = await insertReturning(adapter.db, users, data)
// Parse mutation counts
const result = await adapter.db.delete(posts).where(eq(posts.draft, true))
const deleted = adapter.getMutationCount(result)
Adapter API
interface DatabaseAdapter {
engine: DatabaseEngine // Engine identifier
db: any // Drizzle database instance
atomic<T>(fn): Promise<T> // Run operations atomically
getMutationCount(result): number // Parse affected rows
supportsReturning: boolean // Does the engine support RETURNING?
supportsNativeBatch: boolean // Does the engine have batch()? (D1, Turso)
supportsTransactions: boolean // Is atomic() rolled back on error? (false on D1)
}
Legacy globalThis.__autoApiDb
Setting globalThis.__autoApiDb still works: it is wrapped in a better-sqlite3 adapter with a deprecation
warning. Switch to initializeDatabase(), which also tells the adapter which engine it talks to.
// Legacy (still works, deprecated)
globalThis.__autoApiDb = db
// Recommended
initializeDatabase(db, 'better-sqlite3')
Plugin System
The plugin system provides a structured architecture for extending @websideproject/nuxt-auto-api. Plugins can hook into both the build-time module setup and the server-side runtime pipeline.
Custom Endpoints
@websideproject/nuxt-auto-api provides two approaches for building custom server endpoints that integrate with the auto-api pipeline: createEndpoint() for full pipeline integration, and standalone helpers for lightweight use in regular Nitro handlers.