·
7 min read

Connecting a MySQL Database in Nuxt with Drizzle ORM

Connecting a MySQL Database in Nuxt with Drizzle ORM Image

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 instance
  • server/database/schema - Drizzle schema definitions
  • server/database/migrations - migrations (if using drizzle-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.

Package names and exports evolve; check the Drizzle docs for the current import paths.

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:

server/db/utils/drizzle.ts
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

server/database/schema/publishedArticles.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.
drizzle.config.ts
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:

server/api/articles.get.ts
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 or server/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.
API specifics (methods, return types) can slightly differ across Drizzle releases. If a method name changes, refer to the Drizzle docs.

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, and ORDER 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 tune connectionLimit 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.
Take a look at my Nuxt Starter Kit which is a production-ready foundation for building modern web apps with Nuxt 4+. And of course it uses Drizzle 😇.

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.

I will never share any of your personal data. You can unsubscribe at any time.
If you found this article helpful.You will love these ones as well.
Create an RSS Feed With Nuxt 3 and Nuxt Content v2 Image

Create an RSS Feed With Nuxt 3 and Nuxt Content v2

How I Replaced Revue With a Custom-Built Newsletter Service Using Nuxt 3, Supabase, Serverless, and Amazon SES Image

How I Replaced Revue With a Custom-Built Newsletter Service Using Nuxt 3, Supabase, Serverless, and Amazon SES

Why I Developed My Own Nuxt Starter Kit for SaaS Products Image

Why I Developed My Own Nuxt Starter Kit for SaaS Products

Build and Deploy a Serverless GraphQL React App Using AWS Amplify Image

Build and Deploy a Serverless GraphQL React App Using AWS Amplify