
Michael Hoffmann
@mokkapps

I’ve been integrating databases with Nuxt apps for years, and lately I’ve been leaning heavily on Drizzle ORM for its type-safety, modern API, and good developer experience.
In this post I’ll walk you through connecting a MySQL database to a Nuxt 3+ app using Drizzle - from project setup all the way to query patterns, testing, and performance tips. I’ll show concrete examples you can copy into your project, and explain the reasoning behind key decisions.
I picked Nuxt because of its excellent full‑stack support via Nitro (server API routes, server middleware, and easy runtime config). Drizzle ORM fits nicely into that stack because it’s designed for modern TypeScript-first workflows, and it plays well with database drivers like mysql2. Together they give you:
Before we dive into the code, a small note about continuous learning: adopting new tools like Drizzle is part of staying current. Industry research shows that developers consider continuous upskilling essential - 89% say it’s vital for career growth - and 68% see a skills gap in modern languages and tools. If you’re reading this, you’re already taking steps to bridge that gap. See the reports from ZipDo and Ecomuch for context.
I prefer starting with Nuxt 3 for server routes and Nitro. If you don’t have a project yet:
npx nuxi init my-nuxt-drizzle-app cd my-nuxt-drizzle-app pnpm install Project structure I use:
server/utils - database connection and exported database instanceserver/database/schema - Drizzle schema definitionsserver/database/migrations - migrations (if using drizzle-kit)server/api - Nitro API handlers that use the databaseKeep DB code strictly on the server side — never import it into client components. Nuxt’s directory structure makes this straightforward.
Install the packages you’ll need:
drizzle-orm (core)mysql2 (driver)drizzle-orm/mysql2 (Drizzle binding for mysql2)drizzle-kit (optional, for migrations)Install them:
pnpm add drizzle-orm mysql2 pnpm add -D drizzle-kit If you prefer the named binding import, Drizzle exposes a mysql2 adapter you can import from (the package surface may be provided by the drizzle-orm package). In code we’ll import drizzle from the mysql2 adapter and create a connection with mysql2/promise.
Keep your connection and Drizzle initialization in a single server-only module so other server files can import the configured database:
import { drizzle } from 'drizzle-orm/mysql2' import mysql from 'mysql2/promise' import * as schema from '../database/schema' export { and, asc, desc, eq, or, sql } from 'drizzle-orm' export const tables = schema export async function useDrizzle () { const { private: { databaseUrl } } = useRuntimeConfig() const connection = await mysql.createConnection(databaseUrl) return drizzle({ client: connection, mode: 'default', schema }) } export type PublishedArticle = typeof schema.publishedArticles.$inferSelect nuxt.config.ts -> runtimeConfig.private) to inject DB credentials without bundling them.server/ so it’s not included in the client bundle.Also create nuxt.config.ts runtime config entries:
export default defineNuxtConfig({ runtimeConfig: { // private values only available on server private: { databaseUrl: '' } // public: { ... } if you need client-visible values (you shouldn't for DB creds) } }); If you plan to use drizzle-kit for migrations, add a drizzle.config.ts (example later).
Drizzle uses a schema definition API that is strongly typed. For MySQL you’ll define tables with mysqlTable and typed columns.
Example schema: server/database/schema.ts
import { datetime, int, mysqlTable, text } from 'drizzle-orm/mysql-core' export const publishedArticles = mysqlTable('published_articles', { id: int('id').primaryKey().autoincrement(), published_at: datetime('published_at').notNull(), title: text('title').notNull(), url: text('url').notNull().unique(), }) Notes and best practices:
server/schema to keep separation of concerns.drizzle-kit rather than hand-editing. A migration tool helps maintain schema across environments.import { defineConfig } from 'drizzle-kit' export default defineConfig({ dbCredentials: { url: process.env.NUXT_PRIVATE_DATABASE_URL!, }, dialect: 'mysql', out: './server/database/migrations', schema: './server/database/schema.ts', }) Run migrations with drizzle-kit CLI after installing it as a dev dependency.
With useDrizzle exported from server/utils/drizzle.ts, you can use Drizzle in Nitro API routes and server handlers. Example API route to read published articles:
export default defineEventHandler(async () => { const db = await useDrizzle() const all = await db.select().from(publishedArticles).orderBy(desc(publishedArticles.publishedAt)).limit(100); return all; }); Tips:
server/api or server/routes). Accessing database on client is both insecure and impossible with server-only modules.Security is a must. Here’s how I lock this down in Nuxt:
Finally, don’t commit any .env files or migration SQL with credentials into version control. Use CI secrets / environment variables.
Tests should be deterministic and isolated. My go-to patterns:
Example docker-compose.test.yml for CI:
version: '3.8' services: mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test_db MYSQL_USER: test_user MYSQL_PASSWORD: test_pass ports: - "3307:3306" healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s retries: 10 In your test setup:
drizzle-kit) programmatically or via CLI.Integration tests are slower but give the highest confidence. For CI, spin up MySQL in a job service or via Docker-in-Docker.
I optimize on three layers: schema/indexing, connection usage, and query patterns.
Schema & Indexing:
WHERE, JOIN, and ORDER BY clauses.Connection & Pooling:
mysql2.createPool) and tune connectionLimit according to your server’s concurrency.server/utils/drizzle.ts module handles this).Query Patterns:
SELECT *).LIMIT with pagination instead of fetching massive result sets.mysql2 does this under the hood when you pass parameters rather than string interpolation).Caching:
Monitoring:
EXPLAIN to understand costly queries.Putting Nuxt and Drizzle together gives you a fast, type-safe, server-first way to work with MySQL. To recap the flow I use in production:
mysql2 pools + drizzle(pool) for stable connections.drizzle-kit and keep migrations in source control (without secrets).And one last personal note: learning and adapting to new tools is a key part of being a developer today. As I mentioned earlier, studies show that a large majority of developers consider continuous learning essential, and many see skill gaps in modern tooling. Diving into Drizzle and Nuxt is precisely the kind of skill upgrade that helps keep you productive and marketable - and it’s the kind of hands-on learning I try to prioritize in my own workflow.
Why I Switched Back From VS Code to IntelliJ IDEA: A Developer's Journey
Vercel Acquires NuxtLabs: What This Means for the Future of Nuxt




