TypeScript with Drizzle ORM: Type-Safe Database Access Without Code Generation
AI generated
type
TypeScript
Drizzle ORM
type-safe database access straight from a TypeScript schema, no code generation

No separate generation step, no client rebuild after every schema change: Drizzle derives query types live from the TypeScript schema.

10 min read TypeScript 5.x Database

1. Why Drizzle is different from Prisma and friends

Most strongly typed TypeScript ORMs, Prisma foremost among them, require an explicit code generation step: a separate schema file is used to generate a client, which is then imported. Change the schema, and that step must run again before types are correct in the editor.

Drizzle skips this step entirely. The schema is written directly as TypeScript code, and query types are derived from that exact code via TypeScript's own type inference, with no external code generator sitting in between.

The practical effect: a schema change is visible in the editor immediately, there's no forgotten generate command leading to stale types, and the whole build process has one fewer step, which saves time especially in CI pipelines.

2. Schema definition as TypeScript code

A Drizzle schema consists of ordinary exported function calls like pgTable, describing columns and their types declaratively. There is no dedicated schema language and no separate parser; the schema is, at runtime, perfectly normal, executable JavaScript code.

This closeness to plain TypeScript also means schema definitions can be refactored, split into modules, and handled with ordinary TypeScript tools like ESLint or Prettier just like any other code, with no special tooling for a proprietary schema language.

Relationships between tables are declared via foreign key references directly on the column definition, making the schema's structure readable to anyone already familiar with SQL DDL, without learning an additional abstraction layer.


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

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: text("email").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  authorId: integer("author_id").notNull().references(() => users.id),
});

3. Type inference: from schema to query result

Every query run through the Drizzle client has a fully inferred return type matching exactly the selected columns. If a query selects only two of five columns, the return type also contains exactly those two fields, no any, no manual type annotation.

This precision comes from generic type parameters threaded through the chain of called methods (select, from, where, leftJoin). TypeScript's compiler resolves the final type entirely at compile time, there is no runtime reflection.

For commonly needed shapes, InferSelectModel and InferInsertModel can also derive standalone types, useful for DTOs in an API layer, without having to maintain the field list twice manually.


import { eq } from "drizzle-orm";
import type { InferSelectModel } from "drizzle-orm";

// The return type is fully derived from the query:
const result = await db
  .select({ id: users.id, email: users.email })
  .from(users)
  .where(eq(users.id, 1));
// typeof result[number] === { id: number; email: string }

type User = InferSelectModel<typeof users>;

4. Migrations with drizzle-kit

The companion CLI tool drizzle-kit compares the current TypeScript schema against the database's state and generates SQL migration files from the difference. These files are plain, readable SQL living in the repository, no binary or proprietary intermediate format.

The drizzle-kit generate command creates a new migration file based on the diff against the last known schema state, while drizzle-kit migrate actually runs pending migrations against the target database.

Because the generated SQL files are readable and version-controlled, they can be reviewed in a code review before being run, a level of clarity not always available with automatically generated ORM migrations from other tools.


npx drizzle-kit generate   # creates an SQL migration file from the schema diff
npx drizzle-kit migrate    # runs pending migrations
npx drizzle-kit studio     # opens a local database GUI

5. Relational queries with db.query

Alongside the SQL-close query builder, Drizzle offers db.query, a declarative API for relational queries that returns nested objects instead of flat join results, similar to the convenience many know from Prisma.

The relation definitions needed for this are declared separately from the table schema via the relations function, which cleanly separates schema and relationship logic and keeps both parts independently readable.

The return type of a db.query call with embedded relations is again fully inferred, including correctly typed, optionally present nested arrays for one-to-many relationships.


import { relations } from "drizzle-orm";

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));

const result = await db.query.posts.findMany({
  with: { author: true },
});
// result[number].author is fully typed, no any

6. Type-safe filters and where conditions

Filter functions like eq, gt, inArray, or and are generically typed over their respective column, so a comparison between a text column and a numeric literal is flagged at compile time, rather than surfacing as a database error at runtime.

This type safety extends to composite conditions as well: and(eq(users.id, 1), gt(posts.createdAt, someDate)) combines conditions across different tables, with each individual sub-condition still checked against its correct column definition.

Raw SQL remains accessible at any point, via the sql template function, for cases where the query builder doesn't cover a specific database function, and the return type can be explicitly annotated there too.

7. Drizzle with different database drivers

Drizzle is built driver-agnostic: the same query builder works with PostgreSQL, MySQL, and SQLite, each via a thin adapter to the actual driver, such as node-postgres, mysql2, or better-sqlite3.

This keeps the core library small, since it implements no networking layer of its own but builds on established, already existing drivers. That reduces the attack surface and keeps Drizzle close to each driver's native performance.

For edge environments like Cloudflare Workers or Vercel Edge Functions, there are additional HTTP-based driver adapters, for instance for Neon or Turso, making Drizzle usable outside classic Node.js server environments too.

8. Testing strategies with Drizzle

For integration tests, a real, isolated test database per test run works best, because Drizzle's generated SQL migrations apply unchanged against a fresh SQLite or Postgres instance, with no mock layer between the test and real SQL.

With SQLite in in-memory mode, entire test suites can run in milliseconds against a real database, which usually makes mocking the query builder unnecessary while still verifying real SQL behavior rather than mocked behavior.

For pure unit tests of business logic that wraps Drizzle queries, dependency injection of the database instance is recommended, so tests can substitute a test database for the production connection without altering the actual query logic.

9. Drizzle vs. Prisma vs. Kysely compared

All three tools pursue type-safe database access in TypeScript, but differ noticeably in philosophy and abstraction level: Prisma with its own schema and code generation, Kysely as a pure SQL query builder with no schema-definition layer, Drizzle in between with a TypeScript-native schema and no code generation.

The table below lays out the key differences.

Trait Drizzle Prisma Kysely
Schema format TypeScript code Own .prisma language Manually defined interfaces
Code generation needed No Yes, on every schema change No
Query style SQL-close, builder pattern Fluent API, abstracted from SQL Pure SQL builder
Bundle size Small Larger (Rust engine underneath) Very small
Relational queries db.query API in addition to joins Built-in, very convenient Manual via joins

Mironsoft

TypeScript migration, type safety, and team onboarding

A JavaScript codebase without type safety, but no time for a full migration?

We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.

Migration Roadmap

Plan and execute a gradual JS-to-TS migration without big-bang risk.

Strict Mode Rollout

Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.

Team Onboarding

Bring developers up to speed on TypeScript best practices with workshops and reviews.

10. Summary

Drizzle ORM

Core idea

Schema is TypeScript code; query types arise from inference, no code generation.

Migrations

drizzle-kit generates readable, version-controlled SQL from the schema diff.

Drivers

PostgreSQL, MySQL, SQLite, and edge HTTP drivers via thin adapters.

Positioning

Closer to SQL than Prisma, more convenient with relations than Kysely.

11. FAQ: Drizzle ORM

1Does Drizzle need a build or generation step before use?
No, the schema is directly executable TypeScript code, and query types are derived at compile time through ordinary TypeScript inference. A separate code generator like Prisma's is not needed.
2Does Drizzle support transactions?
Yes, via db.transaction, which passes a callback a database instance bound to the transaction. If an error occurs inside the callback, a rollback happens automatically.
3Can I run raw SQL with Drizzle when the query builder isn't enough?
Yes, the sql template function lets you embed arbitrary SQL, with placeholders still safely parameterized, and the return type can be explicitly annotated.
4How do I migrate an existing Prisma project to Drizzle?
There is no official automatic migration tool between the schema formats; the table structure has to be recreated manually as a Drizzle schema. drizzle-kit can then introspect an existing database to produce a starting schema.
5Is Drizzle suitable for serverless and edge environments?
Yes, especially through HTTP-based drivers for providers like Neon or Turso, which don't require a persistent TCP connection and work well with Cloudflare Workers or Vercel Edge Functions.
6How are many-to-many relationships modeled in Drizzle?
Through an explicit junction table with two foreign keys, exactly as in plain SQL. The relations function then describes both sides of the relationship for convenient db.query access.
7Does drizzle-kit generate migration files automatically at application startup?
No, migration generation and execution are deliberately separate, manually triggered CLI commands, to avoid accidental schema changes in production.
8Can I split Drizzle schemas across multiple files?
Yes, since the schema consists of ordinary TypeScript exports, it can be modularized like any other code and combined via import/export, with no restrictions from a proprietary schema language.
9How good is editor support for complex joins?
Very good, because every method in the query builder threads generic type parameters through, and the editor shows the exact current result type via hover at every intermediate step, even across multiple chained joins.
10Is Drizzle worth it for a small project with few tables?
Yes, the entry cost is low since no code generation step needs configuring and the schema is immediately readable TypeScript code, even for just two or three tables.