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.

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

EnginePackageatomic()ReturningNative Batch
better-sqlite3better-sqlite3transaction (BEGIN IMMEDIATE)YesNo
postgrespg / postgresdb.transaction()YesNo
mysqlmysql2db.transaction()No — rows are read backNo
d1Cloudflare D1not a transaction (see below)YesYes
turso@libsql/clientdb.transaction()YesYes
planetscale@planetscale/database (v1 — Drizzle 0.45 does not support v2)db.transaction()No — rows are read backNo

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():

WriteAll-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 deletionIdevery engine
M2M sync / add / remove / batch syncevery 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')

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.