My new go-to data layer is Drizzle + Turso

Typescript + Drizzle + Turso is a powerful delight

I think my new tech stack is bun/node + drizzle + turso. The purpose of this article is to prove this for myself and document the setup.

First off, some definitions -

  • bun and node are both server side runtimes of the Javascript programming language. Bun can also run Typescript natively.
  • drizzle is an ORM, or Object Relational Mapper that uses Typescript to define database architecture and relationships, and serves as the API for interacting with said database. - https://orm.drizzle.team/
  • turso is a cloud database solution that runs a forked version of SQLite. I like turso because it's easy to use and the free tier is the most generous I've seen on the internet. - https://docs.turso.tech/introduction

In short, I like this stack for the following reasons -

  • I am most comfortable writing applications in Typescript
  • As far as I've seen, drizzle serves wonderfully as a frontend for your database in turso
  • The entire stack is simple to set up!

A demo -

We're going to take this from the top. Steps -

  1. Spin up a bun application
  2. install dependencies to allow usage of drizzle and turso
  3. create turso database
  4. create the database schema
  5. perform basic CRUD operations to confirm setup!

Cool. Drizzle has great documentation on how to do exactly this here.

Spinning up the Bun application

1mkdir drizzle-turso-integration 2cd drizzle-turso-integration 3bun init
zsh

I accept all the basic settings and have my project setup.

Installing dependencies

Bun dependencies

1bun add drizzle-orm 2bun add -D drizzle-kit 3 4bun add dotenv 5bun install @libsql/client
bash

Turso CLI -

brew install tursodatabase/tap/turso

Create Turso database

Now that we've got everything installed, it's time to use the turso CLI to make our database.

At this point it's really just following the steps here - https://orm.drizzle.team/learn/tutorials/drizzle-with-turso#setup-turso-and-drizzle-orm

Within the documentation, we -

  • Create our new database in turso
  • get our connection keys
  • wire put them in our .env file and set up the boilerplate connection code

Create the database schema

We can continue referencing this - https://orm.drizzle.team/learn/tutorials/drizzle-with-turso#setup-turso-and-drizzle-orm

During this step, we -

  • create the database schema in typescript with drizzle using the following syntax -
1import { sql } from "drizzle-orm"; 2import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 3 4export const usersTable = sqliteTable("users", { 5 id: integer("id").primaryKey(), 6 name: text("name").notNull(), 7 age: integer("age").notNull(), 8 email: text("email").unique().notNull(), 9});
typescript
  • create a top level drizzle.config.ts file to house your global drizzle settings.
1import { config } from 'dotenv'; 2import { defineConfig } from 'drizzle-kit'; 3 4config({ path: '.env' }); 5 6export default defineConfig({ 7 schema: './src/db/schema.ts', 8 out: './migrations', 9 dialect: 'sqlite', 10 driver: 'turso', 11 dbCredentials: { 12 url: process.env.TURSO_CONNECTION_URL!, 13 authToken: process.env.TURSO_AUTH_TOKEN!, 14 }, 15});
typescript
  • generate the migrations file for the database (the change log from which it will build) and then apply those migrations to create or modify database table(s).
1bunx drizzle-kit generate 2bunx drizzle-kit migrate

Confirm success

First off, now that I've applied the database migrations, I can use drizzle studio to serve as a UI for my database and confirm the tables I expect to exist do indeed exist.

bunx drizzle-kit studio

This spins up a UI at https://local.drizzle.studio, and I can clearly see that my migrations were applied, based on the existence of the posts and users table.

Drizzle studio UI confirms that we successfully built our tables

Not only can I see my data or currently lack thereof, but I can also run SQL directly against the database via the SQL runner, or write drizzle TS api code in Drizzle Runner. I foresee that being super helpful in situations where you want to test your application queries without needing to run your application. Build the queries first in Drizzle runner and then insert them into your application with confidence.

Next I'm going to add a couple of users and give them a couple of posts, and then query the database for posts by user. This should be a pretty simple Hello World sort of confirmation that this has all worked.

Creating users

1import { db } from "../index"; 2import { InsertUser, usersTable } from "../schema"; 3 4export async function createUser(data: InsertUser) { 5 await db.insert(usersTable).values(data); 6}
typescript

My index.ts file at root level is now

1import { createUser } from "./src/db/mutations"; 2 3createUser({ name: "Pioneer", age: 102, email: "weouthere@example.com" }); 4createUser({ 5 name: "Jefferford", 6 age: 39, 7 email: "forderjeffington@example.com", 8}); 9createUser({ name: "Brockhampton", age: 54, email: "deathgrips@example.com" });
typescript

So when I run the app, it will add this user to the database. The great thing with the drizzle + typescript integration is that as soon as I removed a required field, it alerted me that it was missing. This way, the source of truth is the database schema, and drizzle enforces it in the application layer.

bun index.ts annnnd, boom - Drizzle Studio now shows the users I just created

Now let's make some a post.

1import { db } from "../index"; 2import { InsertPost, postsTable } from "../schema"; 3 4export async function createPost(data: InsertPost) { 5 await db.insert(postsTable).values(data); 6}
typescript
1import { createPost } from "./src/db/mutations"; 2 3createPost({ 4 title: "We out here", 5 content: "MY SPOON......... IS TOO BI-YIG", 6 userId: 1, 7});
typescript

Tadaaaa - we see the created post in drizzle studio

I can also now see the foreign key relationship in in the users table. drizzle studio displays the FK relationship between the users and posts in the users table

And now we confirm everything returns as expected in the application -

1import { eq } from "drizzle-orm"; 2import { db } from "./src/db"; 3import { postsTable } from "./src/db/schema"; 4 5const userOnePosts = await db 6 .select() 7 .from(postsTable) 8 .where(eq(postsTable.userId, 1)); 9const userTwoPosts = await db 10 .select() 11 .from(postsTable) 12 .where(eq(postsTable.userId, 2)); 13console.log({ userOnePosts, userTwoPosts });
typescript

console logs -

1{ 2 userOnePosts: [ 3 { 4 id: 1, 5 title: "We out here", 6 content: "MY SPOON......... IS TOO BI-YIG", 7 userId: 1, 8 createdAt: "2024-07-07 18:12:19", 9 updateAt: 2024-07-07T18:12:18.000Z 10 } 11 ], 12 userTwoPosts: [] 13}
json

And just like that, we're fully wired up. I'm still getting over how painless this process has been, and am excited by the potential.