Skip to content
Learni
View all tutorials
Base de données

How to Get Started with Drizzle ORM in 2026

Lire en français

Introduction

Drizzle ORM is a lightweight, high-performance ORM for TypeScript, designed for developers who want type-safe SQL without sacrificing flexibility. Unlike Prisma, which generates JavaScript, Drizzle compiles your schemas into native SQL queries for top speed and seamless integration with PostgreSQL, MySQL, or SQLite.

Why use it in 2026? Modern apps demand high performance and type safety. Drizzle shines in Next.js, Vercel, or Deno projects with zero bundle size and automated migrations via Drizzle Kit. This beginner tutorial walks you through building a complete users database: schema, migrations, and CRUD. At the end, you'll have a functional, scalable project. Think of it as 'SQL with TypeScript superpowers' – simple, fast, and powerful.

Prerequisites

  • Node.js 20+ installed
  • Basic TypeScript knowledge
  • An editor like VS Code with the Drizzle extension
  • No external database needed: we'll use SQLite for simplicity

Initialize the Project

terminal
mkdir drizzle-tutorial
cd drizzle-tutorial
npm init -y
npm install drizzle-orm better-sqlite3
dnpm install -D drizzle-kit typescript @types/node tsx

We create a new Node.js project and install Drizzle ORM with better-sqlite3 for a local database without complex setup. Dev dependencies include drizzle-kit for migrations and tsx to run TypeScript directly. This sets up a minimal, functional environment.

Configure TypeScript

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "skipLibCheck": true,
    "jsx": "react-jsx",
    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules", "dist"]
}

This standard tsconfig.json enables strict mode and ES2022 for modern compatibility. It lays the groundwork for type-safe Drizzle schemas, preventing common typing errors from the start.

Define the Database Schema

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

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

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

We define a 'users' table with an auto-incrementing id, name, and unique email. User and NewUser types are automatically inferred for full type safety. This forms the foundation: Drizzle turns this schema into valid SQL.

Configure Drizzle Kit

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 configures Drizzle Kit to generate SQL migrations from the schema. It points to a local SQLite file (sqlite.db created automatically). The 'sqlite' dialect ensures perfect compatibility without an external server.

Generate and Apply Migrations

terminal
npx drizzle-kit generate
npx drizzle-kit migrate

The first command generates a SQL file in /drizzle based on schema.ts. The second runs it on sqlite.db, creating the users table. Always run generate before migrate to detect schema changes.

Connect to the Database

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

const sqlite = new Database('./sqlite.db');
export const db = drizzle(sqlite, { schema: { users } });

We connect Drizzle to SQLite using better-sqlite3. The exported db instance is reusable everywhere. The schema option injects types for IntelliSense-powered queries.

Implement Full CRUD Operations

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

// INSERT
async function createUser(newUser: NewUser) {
  return await db.insert(users).values(newUser).returning();
}

// SELECT all
async function getAllUsers() {
  return await db.select().from(users);
}

// SELECT by ID
async function getUserById(id: number) {
  return await db.select().from(users).where(eq(users.id, id)).limit(1);
}

// UPDATE
async function updateUser(id: number, updates: Partial<NewUser>) {
  return await db.update(users).set(updates).where(eq(users.id, id)).returning();
}

// DELETE
async function deleteUser(id: number) {
  return await db.delete(users).where(eq(users.id, id));
}

export { createUser, getAllUsers, getUserById, updateUser, deleteUser };

This module implements INSERT, SELECT, UPDATE, and DELETE with type-safe WHERE clauses using eq. .returning() fetches the modified data. Partial allows partial updates without overwriting fields.

Run and Test the Script

src/main.ts
import { createUser, getAllUsers, getUserById, updateUser, deleteUser } from './index';

async function main() {
  // Create
  const newUser = await createUser({ name: 'Alice', email: 'alice@example.com' });
  console.log('Created:', newUser);

  // Read all
  const allUsers = await getAllUsers();
  console.log('All users:', allUsers);

  // Read one
  const user = await getUserById(1);
  console.log('User 1:', user);

  // Update
  await updateUser(1, { name: 'Alice Updated' });
  console.log('Updated user:', await getUserById(1));

  // Delete
  await deleteUser(1);
  console.log('After delete:', await getAllUsers());
}

main().catch(console.error);

This script tests the full CRUD flow in sequence. Run it with 'npx tsx src/main.ts' to see results in the console. It showcases the smoothness: no boilerplate, composable queries.

Run the Project

terminal
npx tsx src/main.ts

This command runs the test script. You'll see the database populate, queries execute, and everything clean up. Perfect for verifying everything works without errors.

Best Practices

  • Always use inferred types: User and NewUser prevent 90% of runtime bugs.
  • Migrations in CI/CD: Add 'drizzle-kit generate && migrate' to your pipelines.
  • Transactions for batches: Use db.transaction(async tx => { tx.insert... }) for atomicity.
  • Relations for complex schemas: Add pgTable.relations() from the start.
  • Environment vars for production: Replace './sqlite.db' with process.env.DATABASE_URL.

Common Errors to Avoid

  • Forgetting 'npx drizzle-kit generate' before migrate: leads to out-of-sync schemas.
  • Using raw SQL without eq(): loses type safety and risks injections.
  • No .returning() on INSERT/UPDATE: can't retrieve generated IDs.
  • Ignoring SQLite locks in production: switch to PostgreSQL with Neon or Vercel Postgres for concurrency.

Next Steps

Master relations and indexes with the Drizzle docs. Migrate to PostgreSQL for production. Check out our Learni courses on databases for advanced Next.js + Drizzle training.