
Connecting a MySQL Database in Nuxt with Drizzle ORM

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.
Introduction to Nuxt and Drizzle ORM
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:
- A server-first architecture (Nitro) that avoids shipping DB code to the browser.
- Type-safe schemas and query builders from Drizzle, reducing runtime surprises.
- A small and predictable runtime / bundle on the server side.
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.
Setting Up Your Nuxt Project
I prefer starting with Nuxt 3 for server routes and Nitro. If you don’t have a project yet:
- Create a new Nuxt app:
npx nuxi init my-nuxt-drizzle-app
cd my-nuxt-drizzle-app
pnpm install
- Make sure you have TypeScript enabled in the project (Nuxt prompts for that). I recommend enabling strict TS checks — Drizzle benefits from it.
Project structure I use:
server/utils
- database connection and exported database instanceserver/database/schema
- Drizzle schema definitionsserver/database/migrations
- migrations (if usingdrizzle-kit
)server/api
- Nitro API handlers that use the database
Keep DB code strictly on the server side — never import it into client components. Nuxt’s directory structure makes this straightforward.
Installing Drizzle ORM and MySQL Connector
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
.
Configuring Drizzle ORM for MySQL
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
- Use Nuxt runtime config (
nuxt.config.ts
->runtimeConfig.private
) to inject DB credentials without bundling them. - Keep this file in
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).
Creating and Managing Models in Drizzle
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:
- Define your schema in dedicated files under
server/schema
to keep separation of concerns. - Use appropriate column types and length limits to avoid unnecessarily large storage and to enable query planner accuracy.
- If you need migrations, generate SQL with
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.
Executing Queries in Nuxt with Drizzle
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:
- Validate request payloads (for example with Zod) to keep DB interactions safe.
- Always perform DB operations in server routes (
server/api
orserver/routes
). Accessing database on client is both insecure and impossible with server-only modules. - Use typed schema objects when building queries - Drizzle’s type inference reduces mistakes.
Handling Database Connections Securely
Security is a must. Here’s how I lock this down in Nuxt:
- Use Nuxt runtime config (private) to store DB credentials; don’t export them to the client.
- Store secrets in environment variables (and in your secrets manager when in production).
- Use least privilege DB users: create a DB user that only has necessary rights (no admin/root access).
- Use TLS for DB connections if your provider supports it (set SSL options in mysql2 config).
- Use connection pooling and timeouts:
- set connectionLimit to a reasonable number based on your server concurrency
- set acquireTimeout and connectTimeout to avoid hanging requests
- Avoid logging raw SQL or full DB responses in production logs.
Finally, don’t commit any .env
files or migration SQL with credentials into version control. Use CI secrets / environment variables.
Testing Database Operations in a Nuxt Environment
Tests should be deterministic and isolated. My go-to patterns:
- Use a Docker Compose ephemeral MySQL instance for integration tests.
- Reset schema between tests (automated migrations + teardown).
- For unit tests, mock the database layer or use an in-memory test double when possible (MySQL lacks a standard in-memory DB like SQLite’s memory mode — so use Docker).
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:
- Wait until MySQL is healthy.
- Run migrations (
drizzle-kit
) programmatically or via CLI. - Run tests (Vitest is a great choice with Nuxt).
Integration tests are slower but give the highest confidence. For CI, spin up MySQL in a job service or via Docker-in-Docker.
Optimizing Performance for Database Operations
I optimize on three layers: schema/indexing, connection usage, and query patterns.
Schema & Indexing:
- Index columns used in
WHERE
,JOIN
, andORDER BY
clauses. - Keep rows minimal — avoid storing large JSON blobs if relational tables can solve the problem.
- Normalize where appropriate; use denormalization selectively for read-heavy workloads.
Connection & Pooling:
- Use a connection pool (
mysql2.createPool
) and tuneconnectionLimit
according to your server’s concurrency. - Reuse the pool across requests (the single
server/utils/drizzle.ts
module handles this). - Avoid opening and closing connections per request.
Query Patterns:
- Select only needed columns (avoid
SELECT *
). - Use
LIMIT
with pagination instead of fetching massive result sets. - Batch writes where possible (insert many rows in a single query).
- Use prepared statements (
mysql2
does this under the hood when you pass parameters rather than string interpolation).
Caching:
- Add a cache layer (Redis or in-memory) for hot reads if latency is critical.
- Cache results at application level with TTLs for read-heavy endpoints.
Monitoring:
- Use slow query logging and
EXPLAIN
to understand costly queries. - Measure with APM or logs and adjust indexes/queries based on evidence.
Conclusion and Best Practices
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:
- Keep DB connection and schemas on the server side.
- Use
mysql2
pools + drizzle(pool) for stable connections. - Validate input and use typed schemas to prevent bad writes.
- Manage migrations with
drizzle-kit
and keep migrations in source control (without secrets). - Test using ephemeral MySQL instances in CI and mock the db for unit tests.
- Optimize by indexing, selecting only needed fields, batching, and caching when appropriate.
- Secure credentials with Nuxt runtimeConfig and environment variables; never expose them to the client.
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.