Skip to main content
Deno 2 is finally here šŸŽ‰ļø
Learn more
Build a database app with Drizzle ORM and Deno.

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.

Infer schema with pull

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:

Diagram of table schema in postgres

We’ll run the following command to instrospect the database and populate several files under a ./drizzle directory:

deno --env -A --node-modules-dir npm:drizzle-kit pull

Failed to find Response internal state key
No config path provided, using default 'drizzle.config.ts'
Reading config file '/private/tmp/deno-drizzle-example/drizzle.config.ts'
Pulling from ['public'] list of schemas

Using 'pg' driver for database querying
[āœ“] 2 tables fetched
[āœ“] 8 columns fetched
[āœ“] 0 enums fetched
[āœ“] 0 indexes fetched
[āœ“] 1 foreign keys fetched
[āœ“] 0 policies fetched
[āœ“] 0 check constraints fetched
[āœ“] 0 views fetched

[i] No SQL generated, you already have migrations in project
[āœ“] You schema file is ready āžœ drizzle/schema.ts šŸš€
[āœ“] You relations file is ready āžœ drizzle/relations.ts šŸš€
We use the --env flag to read the .env file with our database url and the --node-modules-dir flag to create a node_modules folder that will allow us to use drizzle-kit correctly.

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:

// schema.ts
import {
  boolean,
  foreignKey,
  integer,
  pgTable,
  serial,
  text,
  timestamp,
} from "drizzle-orm/pg-core";

export const dinosaurs = pgTable("dinosaurs", {
  id: serial().primaryKey().notNull(),
  name: text(),
  description: text(),
});

export const tasks = pgTable("tasks", {
  id: serial().primaryKey().notNull(),
  dinosaurId: integer("dinosaur_id"),
  description: text(),
  dateCreated: timestamp("date_created", { mode: "string" }).defaultNow(),
  isComplete: boolean("is_complete"),
}, (table) => {
  return {
    tasksDinosaurIdFkey: foreignKey({
      columns: [table.dinosaurId],
      foreignColumns: [dinosaurs.id],
      name: "tasks_dinosaur_id_fkey",
    }),
  };
});
The above represents in code the two tables, dinosaurs and tasks and their relation. Learn more about using Drizzle to define schemas and their relations.

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:

  1. Add more complex database relationships
  2. Implement a REST API using Hono to serve your dinosaur data
  3. Add validation and error handling to your database operations
  4. Write tests for your database interactions
  5. 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.