Build a Database App with Drizzle ORM and Deno
Drizzle ORM is a TypeScript ORM that provides a type-safe way to interact with your database. In this tutorial, we’ll set up Drizzle ORM with Deno and PostgreSQL to create, read, update, and delete dinosaur data:
You can find all the code for this tutorial in this GitHub repo.
šØļø Deno 2 is here. šØļø
With backwards compatibility with Node/npm, built-in package management, all-in-one zero-config toolchain, and native TypeScript and web API support, writing JavaScript has never been simpler.
Install Drizzle
First, we’ll install the required dependencies using Deno’s npm compatibility. We’ll be using Drizzle with Postgres, but you can also use MySQL or SQLite. (If you don’t have Postgres, you can install it here.)
deno install npm:drizzle-orm npm:drizzle-kit npm:pg npm:@types/pg
This installs Drizzle ORM and its associated tools ā drizzle-kit for schema migrations, pg for PostgreSQL connectivity, and the TypeScript types for PostgreSQL. These packages will allow us to interact with our database in a type-safe way while maintaining compatibility with Deno’s runtime environment.
It will also create a deno.json
file in your project root to manage the npm
dependencies:
{
"imports": {
"@types/pg": "npm:@types/pg@^8.11.10",
"drizzle-kit": "npm:drizzle-kit@^0.27.2",
"drizzle-orm": "npm:drizzle-orm@^0.36.0",
"pg": "npm:pg@^8.13.1"
}
}
Configure Drizzle
Next, let’s create a drizzle.config.ts
file in your project root. This file
will configure Drizzle to work with your PostgreSQL database:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: Deno.env.get("DATABASE_URL")!,
},
});
These config settings determine:
- where to output migration files (
./drizzle
) - where to find your schema definition (
./src/db/schema.ts
) - that PostgreSQL as your database dialect, and
- how to connect to your database using the URL stored in your environment variables
The drizzle-kit
will use this configuration to manage your database schema and
generate SQL migrations automatically.
Weāll also need a .env
file in the project root containing the DATABASE_URL
connection string:
DATABASE_URL=postgresql://[user[:password]@][host][:port]/[dbname]
Be sure to replace the login credentials with yours.
Next, let’s connect to the database and use Drizzle to populate our tables.
Define schemas
There are two ways that you can define your table schema with Drizzle. If you
already have Postgres tables defined, you can infer them with pull
; otherwise,
you can define them in code, then use Drizzle to create a new table. We’ll
explore both approaches below.
pull
Infer schema with If you already have Postgres tables before adding Drizzle, then you can
introspect your database schema to automatically generate TypeScript types and
table definitions with the command
npm:drizzle-kit pull
. This
is particularly useful when working with an existing database or when you want
to ensure your code stays in sync with your database structure.
Let’s say our current database already has the following table schemas:
We’ll run the following command to instrospect the database and populate several
files under a ./drizzle
directory:
The above command will create a number of files within a ./drizzle
directory
that define the schema, trackĀ changes, and provide theĀ necessary information for
databaseĀ migrations:
drizzle/schema.ts
: ThisĀ file defines the databaseĀ schema usingĀ Drizzle ORM’s schemaĀ definition syntax.drizzle/relations.ts
: ThisĀ file isĀ intended to defineĀ relationships between tables usingĀ Drizzle ORM’s relationsĀ API.drizzle/0000_long_veda.sql
: A SQL migrationĀ file that containsĀ the SQL codeĀ to create theĀ databaseĀ table(s). TheĀ codeĀ is commented out ā you can uncomment this code ifĀ you wantĀ to run this migration toĀ create the table(s) in aĀ newĀ environment.drizzle/meta/0000_snapshot.json
: A snapshotĀ fileĀ that representsĀ the current state of yourĀ database schema.drizzle/meta/_journal.json
: This fileĀ keepsĀ track of the migrationsĀ that have been applied toĀ your database. It helpsĀ Drizzle ORM knowĀ which migrations have been runĀ and which ones stillĀ need to be applied.
Define schema in Drizzle first
If you don’t already have an existing table defined in Postgres (e.g. you’re starting a completely new project), you can define the tables and types in code and have Drizzle create them.
Let’s create a new directory ./src/db/
and in it, a schema.ts
file, which
we’ll populate with the below:
Once we have defined ./src/db/schema.ts
, we can create the tables and their
specified relationship by creating a migration:
deno -A npm:drizzle-kit generate
Failed to find Response internal state key
No config path provided, using default 'drizzle.config.ts'
Reading config file '/private/tmp/drizzle/drizzle.config.ts'
2 tables
dinosaurs 3 columns 0 indexes 0 fks
tasks 5 columns 0 indexes 1 fks
The above command will create a ./drizzle/
folder that contains migration
scripts and logs.
Interact with the database
Now that we have setup Drizzle ORM, we can use it to simplify managing data in
our Postgres database. First, Drizzle suggests taking the schema.ts
and
relations.ts
and copying them to the ./src/db
directory to use within an
application.
Let’s create a ./src/db/db.ts
which exports a few helper functions that’ll
make it easier for us to interact with the database:
import { drizzle } from "drizzle-orm/node-postgres";
import { dinosaurs as dinosaurSchema, tasks as taskSchema } from "./schema.ts";
import { dinosaursRelations, tasksRelations } from "./relations.ts";
import pg from "pg";
import { integer } from "drizzle-orm/sqlite-core";
import { eq } from "drizzle-orm/expressions";
// Use pg driver.
const { Pool } = pg;
// Instantiate Drizzle client with pg driver and schema.
export const db = drizzle({
client: new Pool({
connectionString: Deno.env.get("DATABASE_URL"),
}),
schema: { dinosaurSchema, taskSchema, dinosaursRelations, tasksRelations },
});
// Insert dinosaur.
export async function insertDinosaur(dinosaurObj: typeof dinosaurSchema) {
return await db.insert(dinosaurSchema).values(dinosaurObj);
}
// Insert task.
export async function insertTask(taskObj: typeof taskSchema) {
return await db.insert(taskSchema).values(taskObj);
}
// Find dinosaur by id.
export async function findDinosaurById(dinosaurId: typeof integer) {
return await db.select().from(dinosaurSchema).where(
eq(dinosaurSchema.id, dinosaurId),
);
}
// Find dinosaur by name.
export async function findDinosaurByName(name: string) {
return await db.select().from(dinosaurSchema).where(
eq(dinosaurSchema.name, name),
);
}
// Find tasks based on dinosaur id.
export async function findDinosaurTasksByDinosaurId(
dinosaurId: typeof integer,
) {
return await db.select().from(taskSchema).where(
eq(taskSchema.dinosaurId, dinosaurId),
);
}
// Update dinosaur.
export async function updateDinosaur(dinosaurObj: typeof dinosaurSchema) {
return await db.update(dinosaurSchema).set(dinosaurObj).where(
eq(dinosaurSchema.id, dinosaurObj.id),
);
}
// Update task.
export async function updateTask(taskObj: typeof taskSchema) {
return await db.update(taskSchema).set(taskObj).where(
eq(taskSchema.id, taskObj.id),
);
}
// Delete dinosaur by id.
export async function deleteDinosaurById(id: typeof integer) {
return await db.delete(dinosaurSchema).where(
eq(dinosaurSchema.id, id),
);
}
// Delete task by id.
export async function deleteTask(id: typeof integer) {
return await db.delete(taskSchema).where(eq(taskSchema.id, id));
}
Now we can import some of these helper functions to a script where we can
perform some simple CRUD operations on our database. Let’s create a new file
./src/script.ts
:
import {
deleteDinosaurById,
findDinosaurByName,
insertDinosaur,
insertTask,
updateDinosaur,
} from "./db/db.ts";
// Create a new dinosaur.
await insertDinosaur({
name: "Denosaur",
description: "Dinosaurs should be simple.",
});
// Find that dinosaur by name.
const res = await findDinosaurByName("Denosaur");
// Create a task with that dinosaur by its id.
await insertTask({
dinosaurId: res.id,
description: "Remove unnecessary config.",
isComplete: false,
});
// Update a dinosaur with a new description.
const newDeno = {
id: res.id,
name: "Denosaur",
description: "The simplest dinosaur.",
};
await updateDinosaur(newDeno);
// Delete the dinosaur (and any tasks it has).
await deleteDinosaurById(res.id);
We can run it and it will perform all of the actions on the database:
deno -A --env ./src/script.ts
What’s next?
Drizzle ORM is a popular data mapping tool to simplify managing and maintaining data models and working with your database. Hopefully, this tutorial gives you a start on how to use Drizzle in your Deno projects.
Now that you have a basic understanding of how to use Drizzle ORM with Deno, you could:
- Add more complex database relationships
- Implement a REST API using Hono to serve your dinosaur data
- Add validation and error handling to your database operations
- Write tests for your database interactions
- Deploy your application to the cloud
š¦ Happy coding with Deno and Drizzle ORM! The type-safety and simplicity of this stack make it a great choice for building modern web applications.
šØļø Want to learn more Deno? šØļø
Check out our new Learn Deno tutorial series, where you’ll learn:
…and more, in short, bite-sized videos. New tutorials published every Tuesday and Thursday.