Drizzle ORM is a modern TypeScript-first tool that makes working with databases easier and less error-prone.
It sits comfortably between raw SQL and TypeScript’s type system. You write your schema your way, and it handles the rest—generating types automatically so your code stays safe, clean, and consistent from top to bottom.
In this guide, you’ll use Drizzle ORM with SQLite to build a quick, type-safe app with full Create, Read, Update, and Delete (CRUD) functionality.
Prerequisites
Before starting this tutorial, ensure you have:
- Node.js 22.x or newer installed
- Basic knowledge of TypeScript and Node.js
- Familiarity with SQL (helpful but not required)
Step 1 — Setting up your Drizzle ORM project
In this section, you’ll set up the basic project structure, configure TypeScript, and prepare your environment to use Drizzle ORM with SQLite.
First, create a new project directory and jump into it:
Initialize the Node.js project with npm to create a package.json file:
Now, configure your project to use ES Modules. To do this, add the "type" field to your package.json file:
This command adds "type": "module" to your package.json, which tells Node.js to treat .js files as ES Modules instead of CommonJS.
Next, install TypeScript and create a basic configuration:
This will create a tsconfig.json file with the default TypeScript settings. Clear out the contents and replace them with the following configuration to enable modern ES Module support:
Next, add a few helpful scripts to your package.json to simplify development:
These scripts make your development workflow smoother by handling common tasks with simple commands.
The build script compiles your TypeScript code into JavaScript using the TypeScript compiler.
When you're actively working on your project, the dev script runs the compiler in watch mode, automatically rebuilding your code whenever changes are saved.
Once your code is compiled, the start script runs the output with Node.js, assuming your entry point is at dist/index.js.
Now install the Drizzle ecosystem packages for working with SQLite:
These packages cover different parts of the Drizzle ecosystem:
drizzle-orm: The core library that includes the query builder, schema definitions, and type-safe interactions.better-sqlite3: A fast, synchronous SQLite driver. Drizzle supports multiple drivers, but this one works great for local development and smaller apps.drizzle-kit: A CLI tool for managing migrations and other development tasks. It’s installed as a dev dependency to keep your production build lightweight.@types/better-sqlite3: Type definitions forbetter-sqlite3, ensuring TypeScript can understand and type-check your SQLite code properly.
Next, create the base project structure by running:
This creates a dedicated directory for your database logic, keeping things clean and modular. The schema folder is where you'll define your tables, while the db folder can hold your Drizzle configuration and connection setup.
With the directory structure in place and all necessary packages installed, you can set up your first Drizzle connection and begin defining your database schema.
Step 2 — Understanding Drizzle components and creating your first schema
Before diving into code, it's helpful to understand the Drizzle's architecture. Instead of a monolithic design, Drizzle follows a decentralized, composition-first approach—each part has a clear, single responsibility and can be used independently.
Here's a quick overview of Drizzle's core components:
- Schema definition: A declarative, TypeScript-based API for defining your database structure.
- SQL driver adapters: Lightweight wrappers around database drivers (like SQLite or Postgres) that standardize how Drizzle communicates with them.
- Query builder: A set of composable functions that generate SQL while preserving full-type safety.
- Type inference: A powerful system that extracts TypeScript types directly from your schema, ensuring consistency throughout your app.
- Migration tools: Provided by
drizzle-kit, these tools help you generate and run database migrations as your schema evolves.
To get started, create a file named src/db/index.ts and add the following code to set up a basic SQLite connection using Drizzle:
This code initializes a connection to an SQLite database and creates a Drizzle instance that will be used for all database operations. The testConnection function provides a simple way to verify the database is working.
For this tutorial, you'll build a simple bookstore management system. Start by defining the schema for the books table.
Create a new file at src/db/schema/books.ts:
This schema definition showcases Drizzle’s clean and declarative approach to defining database tables. Instead of using classes or decorators, Drizzle uses plain function calls like sqliteTable() to define tables.
Each column is defined using type-specific functions that map directly to SQLite’s native types, and constraints are applied using a fluent, chainable syntax.
Key aspects of this approach include:
- clear column definitions using functions like
integer(),text(), andreal()that align with SQLite’s data types - fluent constraint chaining with methods such as
.notNull()and.primaryKey() - compile-time validation of default values to catch errors early
- timestamp handling through the
mode: 'timestamp'option, which helps work around SQLite’s limited date support
Together, these features create a type-safe, readable schema that integrates smoothly with the rest of your TypeScript codebase.
The real power comes from the $inferSelect and $inferInsert utilities. These extract fully typed definitions from your schema—Book for querying existing rows (including auto-generated fields) and NewBook for inserting new records (where fields like id or createdAt may be optional).
This dual-type setup enforces correct usage and helps avoid common mistakes like trying to insert an auto-incremented ID manually.
Now, create a file to export all schemas named src/db/schema/index.ts:
Finally, create a migration config file to set up your database tables. In the root of your project, add a file called drizzle.config.ts with the following content:
This configuration tells Drizzle Kit where to find your schema files, which SQL dialect to use, and where your SQLite database is located.
Drizzle offers multiple ways to manage schema changes through its migration system. You can generate migration files, run them manually, or use the direct push method to apply changes instantly.
In this tutorial, you'll use the direct push approach—the quickest way to sync your schema with the database without creating migration files.
Add the following script to your package.json:
The db:push script directly applies your schema to the database, creating or updating tables as needed. This approach is perfect for development environments or simple applications.
Now, run the push command to create your database tables:
You should see the output confirming that Drizzle has applied your schema to the database:
The output shows that Drizzle successfully read your configuration, connected to the database, and applied your schema changes.
At this stage, your SQLite database has an empty books table ready to store book records with the defined schema.
Step 3 — Adding data to your database
Now that you have a properly defined schema and your database table is set up, it's time to populate it with some initial data. Drizzle provides a SQL-like builder pattern that constructs insert statements with full-type checking.
Create a file named src/add-books.ts to see how data insertion works with Drizzle:
This script shows how to insert data into your SQLite database using Drizzle. It imports the database connection, the books table, and the NewBook type. An array of book objects is created and inserted using db.insert(books).values(bookData).run().
Then, a single book is added with another insert call. The values() method accepts one or more objects that follow the NewBook type, ensuring type safety at compile time. The API is simple, clear, and closely follows SQL syntax.
Now you can run the TypeScript file directly without compiling it first. For convenience, you can add a dev script to your package.json:
Then you can run the TypeScript file like this:
You should see output similar to:
There are several ways to insert data with Drizzle. Here are some alternative approaches:
You can also use the returning() method to get back the inserted data, which is useful for retrieving auto-generated IDs:
For larger insert operations, you might prefer to insert records in batches:
The type safety provided by Drizzle is one of its greatest strengths. If you try to insert a record with missing required fields or incorrect types, TypeScript will catch these errors at compile time:
Similarly, if you try to provide an ID for a table with auto-incrementing primary keys, TypeScript would warn you about this potential mistake.
This type-safety extends to all Drizzle operations, not just inserts, ensuring that your database interactions are consistent and error-free throughout your application.
Now that your database has some books added, you can move on to querying and retrieving them.
Step 4 — Querying data from your database
Now that your database has data, it's time to retrieve it. Drizzle's query builder provides a type-safe way to construct SQL queries without writing raw SQL strings.
Create a file named src/query-books.ts with this simple query to get all books:
This script shows the basic pattern for querying data with Drizzle. It uses db.select() to start the query, specifies the table with .from(books), and then retrieves all records using .all().
Run this file to display a list of books stored in your database:
You should see output similar to:
Now that you've successfully retrieved all books, you can explore other common query patterns.
For example, the following query gets every row from the table:
This generates a basic SELECT * FROM books query. The .all() method returns an array of records.
Use the where() method with a condition to filter results. This example fetches books by a specific author:
The where() method accepts filter operators like eq() (equals), lt() (less than), and gt() (greater than) to create SQL conditions.
You can also sort the results by using orderBy() with direction helpers like desc() or asc():
The orderBy() method accepts direction operators like desc() (descending) and asc() (ascending) to sort the results.
To get a single result instead of an array, use get(). This returns the first match or undefined if none is found:
Drizzle makes querying your database simple and type-safe. The query builder's syntax is intuitive and follows the structure of SQL, making it easy to understand while still providing the benefits of TypeScript's type checking.
In the next section, you'll learn how to update existing records in your database.
Step 5 — Updating records in your database
Now that you know how to add and query data, it's time update existing records. Drizzle provides a straightforward API for updates that follows the same pattern-based approach you've seen in previous operations.
Create a file named src/update-books.ts with the following code:
This script demonstrates a simple update operation. It first finds a book, updates its price, and then verifies the change by fetching the book again.
Run this file to see the update in action:
You should see output similar to:
The update process in Drizzle follows a clear pattern:
You can also perform batch updates that modify multiple records at once:
For calculated updates that depend on current values, use the sql template literal:
With record updates in place, you can now move on to deleting records from your database.
Step 6 — Deleting records from your database
Now that you've implemented Create, Read, and Update operations, it's time to complete the CRUD functionality by delete records.
Create a file named src/delete-books.ts with the following code:
This script demonstrates how to delete a record from your database. It counts the books before deletion, removes a specific book by title, and then counts the books again to verify the deletion.
Run this file to see the deletion in action:
You should see output similar to:
The delete operation follows the same pattern as other Drizzle operations:
Like with updates, the run() method returns metadata about the operation, including the number of rows affected.
You can also delete records by ID, which is a common operation in applications:
For batch deletions, you can use more complex conditions:
It's important to be careful with delete operations, especially when not using a specific condition. If you run a delete without a where clause, it will delete all records from the table:
To prevent accidental deletion of all records, it's a good practice always to include a where clause with your delete operations or to add safeguards in your application.
Final thoughts
You’ve now built a full CRUD app using Drizzle ORM with SQLite. Along the way, you learned how to set up a project with TypeScript, define type-safe schemas, push schema changes, and perform create, read, update, and delete operations.
As you go further, Drizzle supports more advanced features like table relationships, transactions, indexes, constraints, and migration strategies for production.
The same patterns you used here apply as your app grows. For more, check out the official Drizzle ORM docs.