Nuxt Server Data
The server-side persistence layer for a Nuxt + NuxtHub + Cloudflare app. Drizzle ORM models the schema in TypeScript, NuxtHub auto-exposes a db handle, D1 is the runtime database.
This is the server-side counterpart to nuxt-repositories — repositories are the client-side API access layer; this skill covers what's behind the API.
When to use
- Designing a Drizzle schema for D1 / SQLite
- Generating and applying migrations (local + remote)
- Querying inside Nitro server routes (
server/api/*.post.ts) - Sharing types between client and server via
shared/types/ - Avoiding the wrangler-deploy migration gotcha (migrations do NOT auto-apply)
- Testing DB writes with isolation seams
Reference files
- drizzle-d1.md — schema definition (
sqliteTable, json columns,notNull, defaults), querying patterns (select,insert,count, joins), Drizzle conventions - nuxt-hub.md —
hub.dbconfig, the auto-importeddbandschemaexports, migration generation (nuxt-hub db generate), the_hub_migrationstable, local vs remote migration apply, the build-then-wrangler script pattern - server-types.md —
shared/utils/andshared/types/for cross-side type reuse, Drizzle's inferred row types, request/response shape sharing with Zod
Stack at a glance
Vue component
│ $fetch('/api/contact', ...)
▼
server/api/contact.post.ts ← Nitro event handler
│ validates via shared Zod schema
▼
db.insert(submissions).values(...)
│ auto-imported `db` from NuxtHub
▼
Cloudflare D1 ← SQLite at the edge
Quick example
// server/db/schema.ts
import { sql } from 'drizzle-orm'
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const submissions = sqliteTable('submissions', {
id: text('id').primaryKey(),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
name: text('name').notNull(),
email: text('email').notNull(),
preferences: text('preferences', { mode: 'json' }).$type<string[]>(),
status: text('status').notNull().default('new')
})
// server/api/contact.post.ts
import { and, eq, gt, sql } from 'drizzle-orm'
import { submissions } from '../db/schema'
export default defineEventHandler(async (event) => {
const data = await readValidatedBody(event, contactPayloadSchema.parse)
await db.insert(submissions).values({
id: crypto.randomUUID(),
name: data.name,
email: data.email,
preferences: data.preferences
})
return { ok: true }
})
Notice: db is auto-imported by NuxtHub — no import { db } from ... line. Schema is imported by name (submissions) from ../db/schema.
Critical: migrations don't auto-apply
Neither nuxt build, wrangler deploy, nor Cloudflare Workers Builds run migrations. You apply them manually from a local checkout:
// package.json
"scripts": {
"db:migrate:remote": "NITRO_PRESET=cloudflare-module nuxt build && npx wrangler d1 migrations apply DB --remote --config .output/server/wrangler.json"
}
Detail in nuxt-hub.md. This is the single biggest footgun in the stack — if you're seeing "no such table" on prod after a deploy, you forgot this step.
Anti-patterns
- ❌ Querying from
shared/utils/— that code runs in the client bundle too. Server queries go inserver/only. - ❌ Importing
dbexplicitly — it's auto-imported by NuxtHub; an explicit import path is a sign you're working against the framework. - ❌ Putting raw SQL strings inline for filtering — use Drizzle's
eq/and/gtoperators; they're typed against your schema. - ❌ Running migrations against
--remotefrom a CI environment — CI doesn't have your wrangler auth and the build script indb:migrate:remotedoes. Run it locally. - ❌ Reaching for
vi.mock('~/server/db')in tests — seenuxt-testing/test-seams.mdfor the env-guarded seam pattern.
Related
- nuxt-config — Cloudflare Workers deployment,
wrangler.jsongeneration, secrets vs vars - nuxt-forms — the
contact.post.tsroute that consumes thesubmissionstable - nuxt-testing — DB test isolation seam (
/api/_dev/db) - nuxt-repositories — client-side API access (the layer above this one)