Skip to content
Learni
View all tutorials
Développement Backend

How to Set Up Drizzle ORM in 2026

Lire en français

Introduction

Drizzle ORM is a lightweight, TypeScript-first ORM designed for developers who want a type-safe experience without the overhead of tools like Prisma. In 2026, it dominates thanks to its closeness to native SQL, optimal performance, and seamless integration with Next.js or plain Node.js. Unlike traditional ORMs that generate opaque code, Drizzle lets you write schemas in TypeScript that transform into precise SQL queries.

This beginner tutorial guides you step by step to create a complete project: installation, schema definition, DB connection, migrations, and CRUD operations on a Users table with SQLite (ideal for testing without an external server). At the end, you'll have a working script that inserts, reads, updates, and deletes data. Why is it crucial? Drizzle reduces runtime bugs by 80% through type inference, speeds up queries, and simplifies migrations. Ready to bookmark this reference guide? (142 words)

Prerequisites

  • Node.js 20+ installed
  • Basic TypeScript knowledge (types, imports)
  • Editor like VS Code with TypeScript extension
  • No external database required (embedded SQLite)

Initialize the project and install dependencies

terminal
mkdir drizzle-tutorial && cd drizzle-tutorial
npm init -y
npm install drizzle-orm better-sqlite3
drizzle-kit install
npm install -D typescript @types/node tsx
mkdir src
drizzle-kit generate:sqlite

This command initializes a Node.js project, installs Drizzle ORM with the SQLite driver (better-sqlite3 for optimal performance), the migration kit, TypeScript, and tsx for running TS scripts. The final generate:sqlite prepares the migration tools. Run it all at once for a clean setup; avoid global npm install for isolation.

Configure TypeScript and scripts

Before diving into the core of Drizzle, set up TypeScript and add NPM scripts to simplify commands. This ensures error-free compilation and a smooth workflow.

package.json with useful scripts

package.json
{
  "name": "drizzle-tutorial",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "db:generate": "drizzle-kit generate",
    "db:push": "drizzle-kit push:sqlite",
    "db:migrate": "tsx src/migrate.ts"
  },
  "dependencies": {
    "drizzle-orm": "^0.32.0",
    "better-sqlite3": "^10.0.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "drizzle-kit": "^0.24.0",
    "tsx": "^4.19.0",
    "typescript": "^5.6.0"
  }
}

This package.json enables ESM (type: module), adds scripts for dev, migrations (push for SQLite dev), and uses tsx for hot-reload. Copy-paste to replace yours. Pitfall: without "type": "module", TS imports fail.

tsconfig.json for strict TypeScript

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}

Optimized TypeScript config for Drizzle: strict: true enables maximum type-safety, moduleResolution: bundler avoids ESM conflicts. No emission (noEmit) since tsx compiles on the fly. Analogy: like a gatekeeper for your types.

Define the Drizzle configuration

The central config drives migrations and DB introspection. For SQLite, it's ultra-simple.

drizzle.config.ts

drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/schema.ts',
  out: './drizzle',
  dialect: 'sqlite',
  dbCredentials: {
    url: './sqlite.db',
  },
} satisfies Config;

This file points to your TS schema, generates migrations in ./drizzle, and uses local SQLite. satisfies Config ensures type validation. Pitfall: wrong schema path breaks db:generate. Run npm run db:generate after creating the schema.

Create the database schema

Analogy: The schema is like an architectural blueprint—Drizzle uses it to infer types and generate SQL. Let's define a simple users table.

src/schema.ts

src/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

import { relations } from 'drizzle-orm';

export const users = sqliteTable('users', {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
});

export const usersRelations = relations(users, ({ many }) => ({
  // Future relations here
}));

users table with auto-increment ID (mode 'number' for TS), name, and unique email. relations sets up links (e.g., one-to-many). Types inferred automatically for queries. Copy it, then npm run db:generate to see the generated SQL.

Connect to the database

The connection is a thin wrapper around the native driver—no black magic, just Drizzle.

src/db.ts

src/db.ts
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';

const sqlite = new Database('./sqlite.db');

export const db = drizzle(sqlite, { schema });

Creates a SQLite DB handle and exports it via Drizzle for type-safety. ./sqlite.db is auto-created. Import * as schema enables full inference. Use db everywhere for queries.

Apply migrations

For SQLite in dev, push applies directly. In production, use versioned migrations.

Run the migrations

terminal
npm run db:generate
npm run db:push

generate creates SQL from the TS schema, push executes it atomically. Check ./drizzle/ for migrations. Pitfall: without push, your queries fail on missing table.

Implement CRUD operations

Here's the core: a complete script to test INSERT, SELECT, UPDATE, DELETE with perfect types.

src/index.ts - Full CRUD

src/index.ts
import { db } from './db';
import { users } from './schema';
import { eq, like, asc } from 'drizzle-orm';

(async () => {
  // INSERT
  const newUser = await db.insert(users).values({
    name: 'Alice',
    email: 'alice@example.com',
  }).returning();
  console.log('Inserted:', newUser);

  // SELECT all
  const allUsers = await db.select().from(users).orderBy(asc(users.id));
  console.log('All users:', allUsers);

  // SELECT by email
  const user = await db.select().from(users).where(eq(users.email, 'alice@example.com'));
  console.log('User by email:', user[0]);

  // UPDATE
  await db.update(users).set({ name: 'Alice Updated' }).where(eq(users.id, user[0]?.id!));
  console.log('Updated');

  // DELETE
  await db.delete(users).where(eq(users.id, user[0]?.id!));
  console.log('Deleted');

  process.exit(0);
})();

Complete async script: inserts with returning() (all fields returned), selects with eq/like, sorts asc, updates/deletes by ID. Run npm run dev or npx tsx src/index.ts. Inferred types: user[0] is fully typed! Errors on missing fields.

Best practices

  • Always use returning() to avoid extra queries and keep consistency.
  • Migrations in CI/CD: drizzle-kit push in dev, migrate in prod.
  • Explicit relations: Define them early for type-safe joins.
  • Environment vars: Use process.env.DATABASE_URL for prod DB URL.
  • Unit tests: Mock db with vitest for isolated queries.

Common errors to avoid

  • Forget db:push: Non-existent table → SQL error "no such table".
  • Wrong dialect: SQLite ≠ PostgreSQL → broken migrations.
  • No schema in drizzle(): Loss of type inference (runtime errors).
  • ESM without type: module: import * fails → SyntaxError.

Next steps

How to Set Up Drizzle ORM in 2026 | Tutorial | Learni