Drizzle Tutorial: Type-Safe SQL in TypeScript

Drizzle is a TypeScript ORM that mirrors SQL instead of hiding it, giving you full type inference from schema to query result with zero runtime overhead.

Get started - freeLive cloud workspace in your browser · nothing to install
#drizzle#typescript#orm#postgresql#backend

What is Drizzle?

Drizzle is a TypeScript ORM built around a simple idea: if you already know SQL, your ORM should not force you to learn a second, lossy abstraction on top of it. Instead of hiding the database behind a generated client and a bespoke query language, Drizzle gives you a query builder whose method names and structure map almost one-to-one onto SQL clauses. You write db.select().from(users).where(eq(users.id, 1)), and what runs is a single, predictable SELECT statement.

The headline feature is end-to-end type safety with no code generation. You declare your tables once in TypeScript, and Drizzle infers everything else: the shape of rows you insert, the columns you can filter on, and the exact type of every query result. If you add a notNull() column or change a type, the compiler flags every call site that no longer matches. That feedback loop catches a whole class of runtime database errors before you ever deploy.

Drizzle is also deliberately small. The runtime ships as a thin layer over your database driver, so there is no separate query engine binary, no background process, and negligible bundle weight. That makes it a strong fit for serverless functions and edge runtimes, where cold starts and bundle size directly affect latency and cost.

How Drizzle works (key concepts)

Drizzle splits into two packages with distinct jobs. drizzle-orm is the runtime: it wraps your database driver, builds SQL, and returns typed results. drizzle-kit is a development-time CLI that reads your schema and manages migrations. You never import drizzle-kit in application code.

Your schema is plain TypeScript. You define tables with helpers like pgTable and column builders such as integer, varchar, and timestamp imported from drizzle-orm/pg-core. These exports are the single source of truth. Drizzle uses TypeScript inference to derive both the runtime metadata it needs to build queries and the static types your editor checks against, so there is no generated client to regenerate or commit.

The query builder composes SQL incrementally. Each method returns a builder you can chain or store, and operators like eq, and, gt, and desc come from drizzle-orm. Because the builder is typed against your schema, a typo in a column name or a wrong comparison type is a compile error, not an HTTP 500 error at runtime.

Migrations are handled by drizzle-kit through two workflows. In development you can run drizzle-kit push, which diffs your TypeScript schema against the live database and applies changes directly with no SQL files. For production you run drizzle-kit generate to emit versioned SQL migration files from the diff, review them in your pull request, and apply them in order with drizzle-kit migrate. The generated files are real SQL you can audit and edit.

Installing Drizzle

Install the runtime as a dependency and the CLI as a dev dependency. This example uses PostgreSQL through the node-postgres (pg) driver, which is the most common choice for a standard Node backend.

npm install drizzle-orm pg dotenv
npm install -D drizzle-kit tsx @types/pg

Put your connection string in a .env file at the project root:

DATABASE_URL=postgres://user:password@localhost:5432/app

Then create drizzle.config.ts so drizzle-kit knows where your schema lives, where to write migrations, and how to reach the database:

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

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

The dialect field is what selects PostgreSQL behavior; switch it to mysql or sqlite and install the matching driver to target a different database.

Build a typed schema and run real queries

Start with a schema file at src/db/schema.ts. Each pgTable call names a table and maps TypeScript properties to columns. The column builders carry constraints like notNull(), unique(), and identity generation, and Drizzle reads all of it for both migrations and type inference.

import { integer, pgTable, varchar, timestamp } from 'drizzle-orm/pg-core';

export const usersTable = pgTable('users', {
  id: integer().primaryKey().generatedAlwaysAsIdentity(),
  name: varchar({ length: 255 }).notNull(),
  email: varchar({ length: 255 }).notNull().unique(),
  createdAt: timestamp().defaultNow().notNull(),
});

Apply the schema to your database. During early development, push is the fastest loop:

npx drizzle-kit push

Now wire up a connection and write queries. Create src/index.ts, pass your schema to drizzle(), and use the typed builder. Operators are imported from drizzle-orm.

import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { eq } from 'drizzle-orm';
import { usersTable } from './db/schema';

const db = drizzle(process.env.DATABASE_URL!, { schema: { usersTable } });

async function main() {
  // INSERT and return the created row
  const [created] = await db
    .insert(usersTable)
    .values({ name: 'Ada Lovelace', email: 'ada@example.com' })
    .returning();

  // SELECT with a typed WHERE clause
  const found = await db
    .select()
    .from(usersTable)
    .where(eq(usersTable.email, 'ada@example.com'));

  // UPDATE by id
  await db
    .update(usersTable)
    .set({ name: 'Ada L.' })
    .where(eq(usersTable.id, created.id));

  console.log(found);
}

main();

Run it with npx tsx src/index.ts. Notice that created.id is typed as a number and found is typed as an array of user rows, all inferred from the schema with no manual annotations. Inserting a row that omits email, or comparing it against a non-string, fails at compile time.

When you are ready to deploy, switch from push to versioned migrations. Run npx drizzle-kit generate to write SQL files into ./drizzle, commit them, and apply them in your deploy pipeline with npx drizzle-kit migrate.

When to use Drizzle

Reach for Drizzle when you want SQL-level control with TypeScript-level safety. It works well in serverless and edge deployments because there is no query engine binary, no generated client, and a tiny runtime, which keeps cold starts and bundle size low. It also suits teams who are comfortable with SQL and find that heavier ORMs obscure the queries they actually want to run.

Drizzle is a good default when your data model maps cleanly onto relational tables and you value reviewable, version-controlled migrations. The generated SQL is plain text you can inspect, so there are no surprises about what touches your production database.

It is a weaker fit if you want a fully managed, batteries-included data layer with a large ecosystem of plugins and a higher-level API that abstracts SQL away entirely; a tool like Prisma may serve that goal better. But if your priority is predictable SQL, real type inference without code generation, and minimal runtime cost, Drizzle is one of the strongest options in the TypeScript backend ecosystem today.

Frequently asked questions

What is Drizzle?

Drizzle is a lightweight, type-safe ORM for TypeScript and JavaScript. It models your database schema as TypeScript objects and gives you a SQL-like query builder where every column, join, and result is fully typed. It supports PostgreSQL, MySQL, SQLite, and several serverless drivers.

Is Drizzle better than Prisma?

They make different trade-offs. Drizzle has no separate query engine or generated client, runs with near-zero overhead, and its query API stays close to SQL. Prisma offers a higher-level API and broader tooling. Drizzle is often preferred for serverless and edge runtimes where bundle size and cold starts matter.

Does Drizzle require a code generation step?

No. Unlike some ORMs, Drizzle infers types directly from the TypeScript schema you write, so there is no generated client to keep in sync. The only generation step is drizzle-kit producing SQL migration files, which is optional if you use push during development.

What is the difference between drizzle-orm and drizzle-kit?

drizzle-orm is the runtime library you import in application code for connections and queries. drizzle-kit is a dev-only CLI that diffs your schema and generates, applies, or pushes SQL migrations. You install drizzle-orm as a dependency and drizzle-kit as a dev dependency.

Do real production work, free. Live cloud workspaces in your browser - every project you complete lands on your portfolio.

Get started - free
Get started - free