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
mkdir drizzle-tutorial
cd drizzle-tutorial
npm init -y
npm install drizzle-orm better-sqlite3
dnpm install -D drizzle-kit typescript @types/node tsxWe 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
{
"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
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
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
npx drizzle-kit generate
npx drizzle-kit migrateThe 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
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
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
Run and Test the Script
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
npx tsx src/main.tsThis 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.