Back

Schema-First Database Development with Drizzle

Schema-First Database Development with Drizzle

Your TypeScript types say one thing. Your database says another. Queries fail at runtime, and you’re left debugging mismatches that should never have existed.

Schema-first database design with Drizzle solves this by making your TypeScript code the single source of truth. Your schema definitions drive both your application types and your database structure, eliminating the disconnect that causes runtime surprises.

This article explains how Drizzle ORM schema-first development works, when to use different migration workflows, and common pitfalls to avoid.

Key Takeaways

  • Drizzle ORM follows a code-first approach where TypeScript schema definitions serve as the single source of truth for types, queries, and database structure.
  • Drizzle Kit offers two migration paths: push for rapid local iteration and generate/migrate for auditable, team-safe deployments.
  • For brownfield projects with existing databases, use drizzle-kit pull to introspect your schema before generating migrations to avoid recreating tables.
  • Constraint name mismatches between Drizzle’s deterministic naming and manually created databases can trigger unexpected migration statements.

What Schema-First Actually Means in Drizzle

Drizzle is fundamentally code-first. You define tables, columns, and relationships in TypeScript, and that definition becomes authoritative for everything downstream—queries, migrations, and type inference.

import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"

export const posts = pgTable("posts", {
  id: serial().primaryKey(),
  title: text().notNull(),
  content: text(),
  createdAt: timestamp().defaultNow(),
})

This schema file serves dual purposes. Drizzle ORM uses it for type-safe queries. Drizzle Kit uses it to generate or apply database changes.

The term “schema-first” here means your TypeScript definitions lead. The database follows.

Code-First vs Database-First: Understanding the Distinction

Traditional database-first workflows treat the database as authoritative. You create tables manually or with SQL scripts, then generate application code from the existing structure.

Code-first and database-first comparisons often conflate these terms. In Drizzle’s context:

  • Code-first: Your TypeScript schema drives database changes
  • Database-first: You pull existing database structure into TypeScript using drizzle-kit pull

Drizzle supports both. For greenfield projects, code-first is typically cleaner. For brownfield scenarios—joining a project with an existing database—introspection via pull gets you started quickly.

The Drizzle Kit Workflow: Push vs Generate

Drizzle Kit offers two primary paths for applying schema changes to your database.

Schema Push for Rapid Iteration

npx drizzle-kit push

Push compares your schema to the database and applies changes directly. No migration files. No review step.

This works well for:

  • Solo projects
  • Early prototyping
  • Local development databases you can recreate

The tradeoff is visibility. You won’t have a record of what changed or when.

Generated Migrations for Team Safety

npx drizzle-kit generate
npx drizzle-kit migrate

generate creates SQL migration files. migrate applies them. The files live in version control, creating an audit trail.

This approach suits:

  • Team environments where changes need review
  • Production deployments requiring rollback capability
  • Compliance scenarios needing change documentation

Neither method is universally correct. The Drizzle Kit workflow accommodates both based on your context.

Configuring Drizzle Kit

Your drizzle.config.ts tells Drizzle Kit where to find schemas and store migrations:

import { defineConfig } from "drizzle-kit"

export default defineConfig({
  dialect: "postgresql",
  schema: "./src/db/schema.ts",
  out: "./drizzle/migrations",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
})

Drizzle supports major SQL databases including PostgreSQL, MySQL, and SQLite/libSQL, with additional dialects such as MSSQL and CockroachDB emerging. The configuration stays largely consistent across databases—only the dialect and driver-specific options change.

Common Pitfalls to Avoid

Schema-first development with Drizzle isn’t without friction points.

Existing tables require careful handling. If you’re adopting Drizzle on a database with existing tables, your first generate may produce migrations that try to recreate everything. Use pull first to sync your schema file with reality.

Introspection can produce noisy diffs. Pulling from a database may include constraint names or defaults that differ from what you’d write manually. Subsequent generates may flag these as changes even when nothing meaningful changed.

Constraint name mismatches cause unexpected migrations. Drizzle generates constraint names deterministically. If your database has different names (from manual creation or another tool), you’ll see ALTER statements that rename constraints without changing behavior.

Runtime migrations need error handling. If you’re applying migrations programmatically during deployment, wrap them in proper error handling. A failed migration mid-deploy can leave your database in an inconsistent state.

When Schema-First Improves Your Workflow

Drizzle ORM schema-first development shines when:

  • You want compile-time guarantees that queries match your schema
  • Multiple developers need to coordinate database changes
  • You’re deploying to edge or serverless environments where bundle size matters
  • You prefer SQL-like syntax over abstracted query builders

Drizzle Studio complements this workflow with a visual interface for browsing data and testing queries against your schema.

Conclusion

Start with your schema file. Define tables that match your domain. Use push while iterating locally, then switch to generate and migrate before involving teammates or deploying to production.

The goal isn’t choosing one workflow forever—it’s understanding the tradeoffs so you pick the right tool for each phase of development.

FAQs

Yes. Run drizzle-kit pull to introspect your existing database and generate a matching TypeScript schema. This avoids destructive migrations. Once your schema file reflects the current database state, you can begin using generate and migrate for all future changes without risking data loss.

Switch to generate once your project moves beyond solo prototyping. As soon as other developers are involved or you are deploying to staging or production, migration files provide the audit trail and review process you need. Push is best reserved for local development where the database can be safely recreated.

Yes. Drizzle supports defining foreign keys directly in your schema and provides a relations API for declaring one-to-one, one-to-many, and many-to-many relationships. These relations power the relational query API, which lets you fetch nested data in a type-safe way without writing raw SQL joins.

Both are code-first ORMs, but they differ in philosophy. Prisma uses its own schema language and generates a client from it. Drizzle defines schemas in plain TypeScript, giving you direct control over SQL output and smaller bundle sizes. Drizzle is closer to SQL by design, while Prisma abstracts more of it away.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay