Shared database schema with DrizzleORM and Turborepo

Piotr Pliszko

In the last few years, I've had an opportunity to try out multiple ORMs when working with various technologies. In the TypeScript world I'm mostly working in right now I've usually used TypeORM and Prisma, but recently I've been looking for something more lightweight, that will support serverless environments and will be closer to the raw SQL. When looking for some alternatives, I encountered Drizzle ORM and decided to give it a go - and it seems to be a great fit for my current needs!

I also recently started to use Turborepo more often instead of nx, so when starting a new project I decided to use both of them together. The project I created consisted of a SvelteKit app and a standalone server app created with NestJS. Those two did not communicate with each other directly, but they both connected to the same database. Because of that, I've decided that it would be a good idea to share database schema and types between them, making sure that they will be always in sync.

The easiest way to do this in Turborepo is to create a package that will consist of all the things we would like to share and then use it in both apps. Let's see how to do it from scratch.

Idea

First of all, let's think about what we would like to share:

  • database schema - table definitions, relations, etc.
  • types - both select and insert types for each model

Besides sharing some code, a separate database package could be useful to separate some database-related features from the rest of the code. In addition, our package will also contain:

  • migration files
  • script to run migrations
  • utility scripts to e.g. introspect database or push schema changes to the database

Implementation

Source code

If you want to jump straight to the code, the source code for this post is available on GitHub: drizzleorm-turborepo-demo.

I assume that you already have a Turborepo workspace created. If not, take a look at the docs to create one. Also, for the purpose of this post, I will be using PostgreSQL as a database, but you can use any other database supported by DrizzleORM.

Creating a package

To create a database package we will use turbo gen command:

# npm
npx turbo gen workspace --name database --type package

# pnpm
pnpm dlx turbo gen workspace --name database --type package
TypeScript

If you are using TypeScript, remember to create tsconfig.json in the new package if it doesn't exist. In my case I wanted package to be shared between NestJS and SvelteKit, so I had to create two separate configs and for CommonJS and ESM builds, because NestJS doesn't support ESM imports yet. In this post we will focus on building only CommonJS.


Example of a basic tsconfig.json file for CommonJS we will use in this post:

{
  "extends": "../tsconfig/base.json",
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2021",
    "outDir": "./dist"
  },
  "exclude": ["dist", "node_modules"]
}

Now, in the new package, let's create index.ts file with an empty export just to make sure that it's a module.

export {};

Then, add some scripts to build the package. We don't need any bundler in this case, so we will use tsc. Remember to install typescript as a dev dependency.

{
  "name": "database",
  "version": "0.0.0",
  "private": true,
  "main": "dist/index.js",
  "scripts": {
    "dev": "tsc --watch",
    "build": "tsc"
  },
  "devDependencies": {
    "typescript": "^5.2.2"
  }
}

package.json above assumes that out directory configured for TypeScript is dist. If you are using a different one, you need to change it in the main field. Now, using npm run build -- --filter=database command we can build our database package and see if everything works correctly.

❯ pnpm run build --filter=database

> drizzleorm-turborepo-demo@ build /my/project/path/drizzleorm-turborepo-demo
> turbo run build "--filter=database"

• Packages in scope: database
• Running build in 1 packages
• Remote caching disabled
database:build: cache miss, executing 5a8a948183046bae
database:build:
database:build: > [email protected] build /my/project/path/drizzleorm-turborepo-demo/packages/database
database:build: > tsc
database:build:

 Tasks:    1 successful, 1 total
Cached:    0 cached, 1 total
  Time:    1.036s

Perfect! Now we can start adding some code.

Adding DrizzleORM

Let's start with adding DrizzleORM to our package. We will also add postgres dependency because we will be using PostgreSQL as our database.

# npm
npm i drizzle-orm postgres --workspace=database
npm i -D drizzle-kit --workspace=database

# pnpm
pnpm add drizzle-orm postgres --filter database
pnpm add -D drizzle-kit --filter database
Installing dependencies only in database package

As you may have noticed, when installing dependencies we are using --workspace=database or --filter database flags. This is because we want to install dependencies only in the database package, not in the whole workspace. There is a syntax difference between npm and pnpm, but the idea is the same. You can of course cd into specific directory and execute commands there, but if you want to operate from the root of the workspace, they come in handy.

Creating schema

After the package is installed let's take a look into Drizzle docs to learn how to create schema file. This is an example provided by docs:

import { pgTable, serial, text, varchar } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  fullName: text('full_name'),
  phone: varchar('phone', { length: 256 }),
});

Pretty straightforward. So, let's create a schema.ts file in our package and add some tables to it. For this post, I will be using a simple schema with two tables - users and posts. The users table will have a one-to-many relation with the posts table.

Database type

In this post, we are using the Postgres database. If you want to use another type of database, you will need to use different packages and utilities from DrizzleORM. Please refer to Drizzle docs to learn more.

import { pgTable, integer, serial, text, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content').notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  userId: integer('user_id')
    .notNull()
    .references(() => users.id),
});

export const postsRelations = relations(posts, ({ one }) => ({
  takeout: one(users, {
    fields: [posts.userId],
    references: [users.id],
  }),
}));

In addition to these tables, we would also like to have related types that we can use in our code. Create a types.ts file and add them there.

import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
import { posts, users } from './schema';

export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;

export type Post = InferSelectModel<typeof posts>;
export type NewPost = InferInsertModel<typeof posts>;

As you can see, we are creating two separate types for one model - one for select queries and one for insert queries. This is because we don't want to enforce providing all the fields when inserting a new record. For example, we don't want to require id or createdAt fields when inserting a new user. Those are fields that will be automatically generated by the database, so we want them in the select type. If we inspect what types are generated, we can see the difference:

type User = {
  id: number;
  name: string;
  createdAt: Date;
};

type NewUser = {
  name: string;
  id?: number | undefined;
  createdAt?: Date | undefined;
};
More convenient model type inference

In previous versions of DrizzleORM we would do something like this to infer types:

type User = InferModel<typeof users>;
type NewUser = InferModel<typeof users, 'insert'>;

Since drizzle-orm version 0.28.3 InferModel is deprecated in favor of more explicit InferSelectModel and InferInsertModel.

It's also possible to use table.$inferSelect / table._.inferSelect and table.$inferInsert / table._.inferInsert for more convenient table model type inference. Example:

import { InferSelectModel, InferInsertModel } from 'drizzle-orm';

const usersTable = pgTable('users', {
  // ...
});

type SelectUser = typeof usersTable.$inferSelect;
type InsertUser = typeof usersTable.$inferInsert;

type SelectUser2 = InferSelectModel<typeof usersTable>;
type InsertUser2 = InferInsertModel<typeof usersTable>;

Read more about the 0.28.3 release here.

In the meantime, we can also update our index.ts file.

export * from './schema';
export * from './types';

Connecting to the database

We have our schema and types ready, so now we can connect to the database. I've started local instance of PostgreSQL, but of course you can connect to any other database supported by DrizzleORM. If you are looking for some convenient cloud Postgres databases to start with I can recommend taking a look at Supabase or Neon. Both of them have a free tier and are easy to set up.

It's important to protect your database credentials, so we will use environment variables to store them. Create a .env file in the root of your workspace and add the DATABASE_URL variable with your database connection string. In my case I am using just a local Postgres instance with postgres:postgres credentials, so .env file looks like this:

DATABASE_URL='postgresql://postgres:postgres@localhost:5432/drizzle-turbo-demo'

Before we connect, let's add dotenv library to our project. We will use it to load environment variables from .env file.

# npm
npm i dotenv --workspace=database

# pnpm
pnpm add dotenv --filter database

Now, we can create a database.ts file where we will connect to the database and export db object that we will use to run queries.

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as dotenv from 'dotenv';
import * as schema from './schema';

dotenv.config();

const connectionString = process.env.DATABASE_URL || '';
const client = postgres(connectionString);

export const db = drizzle(client, { schema });

Let's see what's happening there:

  • We are importing the drizzle function from drizzle-orm/postgres-js - this will be used to create a database object to use in our code. If you are using something else as your database please refer to Drizzle documentation to see what package you may need to use. For example, if you are using Postgres from Neon you will use '@neondatabase/serverless', and if you are using MySQL with Planetscale you will use 'drizzle-orm/planetscale-serverless'.
  • We are using dotenv.config() to load environment variables from the .env file using the dotenv library.
  • After environment variables are imported we are reading connectionString with an empty string fallback - you can always provide a different default value if you want, but in this case, I would like an app to fail if DATABASE_URL is not provided.
  • Then, we create a client object using the postgres library. The only thing we need to provide here is our connectionString.
  • Finally, we are creating a db object which will be used by our apps to access the database.

Now, let's update our index.ts file with the new exports.

export * from './database';
export * from './schema';
export * from './types';

Drizzle configuration

Because our package will be more than just a shared package, but also a place where we will keep our migrations and database scripts, we will need to add some configuration to Drizzle. We will do it in the drizzle.config.ts file.

import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';

dotenv.config();

export default {
  schema: './schema.ts',
  out: './drizzle',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL || '',
  },
} satisfies Config;

As you can see, we reused the same DATABASE_URL environment variable that we used in the database.ts file. We also specified where we keep our schema and where drizzle should put it's files, and we've specified pg as the driver for Postgres database.

So, now we have a database package with a schema, types, and a database connection. We also have drizzle configuration. Let's see if it works! We can start by adding check command to our package.json file - we will use it to check consistency of the migrations, but also just to check if the connection is working fine.

"db:check": "drizzle-kit check:pg",

We will also add db:check command to the main package.json:

"db:check": "turbo run db:check"

and add it to turbo.json:

{
  "$schema": "https://turbo.build/schema.json",
  // ...
  "pipeline": {
    // ...
    "db:check": {
      "cache": false
    }
  }
}

Let's run npm run db:check and see what happens.

❯ pnpm run db:check

> drizzleorm-turborepo-demo@ db:check /my/project/path/drizzleorm-turborepo-demo
> turbo run db:check

• Packages in scope: database, nextjs-app, sveltekit-app, tsconfig
• Running db:check in 4 packages
• Remote caching disabled
database:db:check: cache bypass, force executing 5088ede8aaebdbde
database:db:check:
database:db:check: > [email protected] db:check /my/project/path/drizzleorm-turborepo-demo/packages/database
database:db:check: > drizzle-kit check:pg
database:db:check:
database:db:check: drizzle-kit: v0.19.13
database:db:check: drizzle-orm: v0.28.5
database:db:check:
database:db:check: No config path provided, using default 'drizzle.config.ts'
database:db:check: Reading config file '/my/project/path/drizzleorm-turborepo-demo/packages/database/drizzle.config.ts'
database:db:check: Everything's fine 🐶🔥

 Tasks:    1 successful, 1 total
Cached:    0 cached, 1 total
  Time:    867ms

Great! It seems that everything is working fine. If we inspect what happened in our files, we can see that Drizzle created a drizzle directory. We will take a closer look at it in a moment.

Generating SQL for the schema

Our schema now exists only in the code, but of course we would like to have it in the database as well. First, we will use the generate command to generate SQL files reflecting our schema. Let's add scripts for convenience.

// database package.json
"db:generate": "drizzle-kit generate:pg",

// main package.json
"db:generate": "turbo run db:generate",

// turbo.json
"db:generate": {
  "cache": false
},
❯ pnpm run db:generate

> drizzleorm-turborepo-demo@ db:generate /my/project/path/drizzleorm-turborepo-demo
> turbo run db:generate

• Packages in scope: database, nextjs-app, sveltekit-app, tsconfig
• Running db:generate in 4 packages
• Remote caching disabled
database:db:generate: cache bypass, force executing 2ec25d04501c485f
database:db:generate:
database:db:generate: > [email protected] db:generate /my/project/path/drizzleorm-turborepo-demo/packages/database
database:db:generate: > drizzle-kit generate:pg
database:db:generate:
database:db:generate: drizzle-kit: v0.19.13
database:db:generate: drizzle-orm: v0.28.5
database:db:generate:
database:db:generate: No config path provided, using default 'drizzle.config.ts'
database:db:generate: Reading config file '/my/project/path/drizzleorm-turborepo-demo/packages/database/drizzle.config.ts'
database:db:generate: 2 tables
database:db:generate: posts 5 columns 0 indexes 1 fks
database:db:generate: users 3 columns 0 indexes 0 fks
database:db:generate:
database:db:generate: [✓] Your SQL migration file ➜ drizzle/0000_medical_lightspeed.sql 🚀

 Tasks:    1 successful, 1 total
Cached:    0 cached, 1 total
  Time:    921ms

Let's see what got generated in the drizzle directory.

❯ exa --tree packages/database/drizzle
packages/database/drizzle
├── 0000_medical_lightspeed.sql
└── meta
   ├── 0000_snapshot.json
   └── _journal.json

In the generated SQL file we can find our schema:

CREATE TABLE IF NOT EXISTS "posts" (
	"id" serial PRIMARY KEY NOT NULL,
	"title" text NOT NULL,
	"content" text NOT NULL,
	"created_at" timestamp with time zone DEFAULT now() NOT NULL,
	"user_id" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
	"id" serial PRIMARY KEY NOT NULL,
	"name" text NOT NULL,
	"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
 ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
 WHEN duplicate_object THEN null;
END $$;

This SQL file can be of course executed manually in the database, or used with some automation tools, but we will use Drizzle to do it for us. Let's add a script to run migrations then! Create a migrate.ts file with the following content:

import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { db } from './index';

migrate(db, { migrationsFolder: 'drizzle' })
  .then(() => {
    console.log('migrations finished!');
    process.exit(0);
  })
  .catch((err) => {
    console.log(err);
    process.exit(1);
  });

Of course, we will add convenience script to run it:

// database package.json
"db:migrate": "ts-node ./migrate.ts",

// main package.json
"db:migrate": "turbo run db:migrate",

// turbo.json
"db:migrate": {
  "cache": false
}

Let's install ts-node to be able to run this script:

# npm
npm i -D ts-node --workspace=database

# pnpm
pnpm add -D ts-node --filter database

Now, we can run npm run db:migrate and see what happens.

❯ pnpm run db:migrate

> drizzleorm-turborepo-demo@ db:migrate /my/project/path/drizzleorm-turborepo-demo
> turbo run db:migrate

• Packages in scope: database, nextjs-app, sveltekit-app, tsconfig
• Running db:migrate in 4 packages
• Remote caching disabled
database:db:migrate: cache bypass, force executing 45b11e78b241b4e0
database:db:migrate:
database:db:migrate: > [email protected] db:migrate /my/project/path/drizzleorm-turborepo-demo/packages/database
database:db:migrate: > ts-node ./migrate.ts
database:db:migrate:
database:db:migrate: migrations finished!

 Tasks:    1 successful, 1 total
Cached:    0 cached, 1 total
  Time:    1.823s

Seems like everything is working fine. Let's check if our schema is in the database. Below you can see a screenshot of ERD generated by pgAdmin.

Entity-relationship diagram in pgAdmin

Making changes to the schema

Everything is looking great so far, but what if we want to make some changes to the schema? Let's say we want to add a draft field to the posts table to indicate if the post is a draft or not. Modify the schema.ts file by adding the new field:

import {
  // ...
  boolean,
} from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  // ...
  draft: boolean('draft').notNull().default(false),
});

After saving, we can generate SQL file reflecting our change.

❯ pnpm run db:generate

> drizzleorm-turborepo-demo@ db:generate /my/project/path/drizzleorm-turborepo-demo
> turbo run db:generate

• Packages in scope: database, nextjs-app, sveltekit-app, tsconfig
• Running db:generate in 4 packages
• Remote caching disabled
database:db:generate: cache bypass, force executing 5c2c70b808246246
database:db:generate:
database:db:generate: > [email protected] db:generate /my/project/path/drizzleorm-turborepo-demo/packages/database
database:db:generate: > drizzle-kit generate:pg
database:db:generate:
database:db:generate: drizzle-kit: v0.19.13
database:db:generate: drizzle-orm: v0.28.5
database:db:generate:
database:db:generate: No config path provided, using default 'drizzle.config.ts'
database:db:generate: Reading config file '/my/project/path/drizzleorm-turborepo-demo/packages/database/drizzle.config.ts'
database:db:generate: 2 tables
database:db:generate: posts 6 columns 0 indexes 1 fks
database:db:generate: users 3 columns 0 indexes 0 fks
database:db:generate:
database:db:generate: [✓] Your SQL migration file ➜ drizzle/0001_eminent_edwin_jarvis.sql 🚀

 Tasks:    1 successful, 1 total
Cached:    0 cached, 1 total
  Time:    872ms

We can again take a look what appeared in our drizzle directory.

❯ exa --tree packages/database/drizzle
packages/database/drizzle
├── 0000_medical_lightspeed.sql
├── 0001_eminent_edwin_jarvis.sql
└── meta
   ├── 0000_snapshot.json
   ├── 0001_snapshot.json
   └── _journal.json

In the newly generated SQL file we can see that the draft field was added to the posts table.

ALTER TABLE "posts" ADD COLUMN "draft" boolean DEFAULT false NOT NULL;

Now, let's run migrations to apply this change to the database.

❯ pnpm run db:migrate

> drizzleorm-turborepo-demo@ db:migrate /my/project/path/drizzleorm-turborepo-demo
> turbo run db:migrate

• Packages in scope: database, nextjs-app, sveltekit-app, tsconfig
• Running db:migrate in 4 packages
• Remote caching disabled
database:db:migrate: cache bypass, force executing 7cf64921326cd641
database:db:migrate:
database:db:migrate: > [email protected] db:migrate /my/project/path/drizzleorm-turborepo-demo/packages/database
database:db:migrate: > ts-node ./migrate.ts
database:db:migrate:
database:db:migrate: {
database:db:migrate:   severity_local: 'NOTICE',
database:db:migrate:   severity: 'NOTICE',
database:db:migrate:   code: '42P06',
database:db:migrate:   message: 'schema "drizzle" already exists, skipping',
database:db:migrate:   file: 'schemacmds.c',
database:db:migrate:   line: '131',
database:db:migrate:   routine: 'CreateSchemaCommand'
database:db:migrate: }
database:db:migrate: {
database:db:migrate:   severity_local: 'NOTICE',
database:db:migrate:   severity: 'NOTICE',
database:db:migrate:   code: '42P07',
database:db:migrate:   message: 'relation "__drizzle_migrations" already exists, skipping',
database:db:migrate:   file: 'parse_utilcmd.c',
database:db:migrate:   line: '207',
database:db:migrate:   routine: 'transformCreateStmt'
database:db:migrate: }
database:db:migrate: migrations finished!

 Tasks:    1 successful, 1 total
Cached:    0 cached, 1 total
  Time:    1.781s
Entity-relationship diagram in pgAdmin

We can see that we have successfully applied the change with the migration.

Down migrations

In a real-life scenario, you probably want to have some down migrations as well to revert changes when needed. In this post we are not going to cover this topic.

Usage

Now, when we have our database package ready, we can use it in our apps. For this post, I've created two demo apps to use our brand new database package - one with SvelteKit, and one with NextJS. Let's see how to use it in both of them.

Source code

The source code for this post is available on GitHub: drizzleorm-turborepo-demo.

Populating database with demo data

To make sure there is something in our database that we can use to display in our apps, let's add some demo data by running this simple SQL script:

INSERT INTO users("name") VALUES
  ('John'),
  ('Jane'),
  ('Joe');

INSERT INTO posts(title, content, user_id, draft) VALUES
  (
    'Title 1',
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ex leo, malesuada id nisi gravida, fringilla sodales nisi. Donec tincidunt, eros et dignissim molestie, mi augue blandit lorem, vitae commodo dolor felis a lacus.',
    1,
    false
  ),
  (
    'Title 2',
    'Maecenas aliquam vitae velit porttitor varius. Fusce sit amet vestibulum elit, et vestibulum nisi.',
    1,
    true
  ),
  (
    'Title 3',
    'Morbi a sollicitudin eros. Cras sit amet ante sed ligula sollicitudin iaculis eu eu dolor. Etiam viverra, est quis varius hendrerit, dui velit pulvinar velit, at tincidunt odio justo eu massa. In ultrices feugiat quam, nec vehicula neque ullamcorper non. Cras et sem consectetur, vestibulum felis ac, scelerisque nibh.',
    3,
    false
  );

This should add few users and posts to our database.

drizzle-turbo-demo=# SELECT * from users;
 id | name |          created_at
----+------+-------------------------------
  1 | John | 2023-08-31 16:42:34.562072+00
  2 | Jane | 2023-08-31 16:42:34.562072+00
  3 | Joe  | 2023-08-31 16:42:34.562072+00
(3 rows)

drizzle-turbo-demo=# SELECT * from posts;
 id |  title  |                    content                    |          created_at           | user_id | draft
----+---------+-----------------------------------------------+-------------------------------+---------+-------
  1 | Title 1 | Lorem ipsum dolor sit amet, consectetur (...) | 2023-08-31 16:42:34.562072+00 |       1 | f
  2 | Title 2 | Maecenas aliquam vitae velit porttitor  (...) | 2023-08-31 16:42:34.562072+00 |       1 | t
  3 | Title 3 | Morbi a sollicitudin eros. Cras sit ame (...) | 2023-08-31 16:42:34.562072+00 |       3 | f
(3 rows)

Next.js

Let's start with the Next.js app. This app will display a list of users loaded from the database. First, we need to add our database package as a dependency in the package.json file.

"dependencies": {
  // ...
  "database": "workspace:*"
}

After adding this as a dependency, we may need to run dependency installation again, for example, pnpm install if you are using pnpm. We will also add dotenv as a dependency to read our .env file.

# npm
npm i dotenv --filter nextjs-app

# pnpm
pnpm add dotenv --filter nextjs-app

Let's now create .env file in the root of the nextjs-app package and add DATABASE_URL variable there. For me it looks like this:

DATABASE_URL='postgresql://postgres:postgres@localhost:5432/drizzle-turbo-demo'
Environment variables

As you can see, we are adding this variable again, even though we already have it in the database package. This is because dotenv in our configuration will load these variables from a particular project directory. We do this just for demo purposes - in real-life projects you probably already have a way to provide environment variables to your apps, different for development and deployment.

In page.tsx I can now import a db object from database package and use it to load users from the database. I will also import users table definition which I will use to in query, and User type that we have created to type the result.

import { db, users, User } from 'database';

async function getData(): Promise<{ users: User[] }> {
  const usersData = await db.select().from(users);
  return { users: usersData };
}

I will now create a super simple user list from loaded data.

export default async function Home() {
  const { users } = await getData();

  return (
    {/* ... */}
      <div>
        <div>Users:</div>
        <ul>
          {users.map((user) => (
            <li key={user.id}>
              {user.id} - {user.name}
            </li>
          ))}
        </ul>
      </div>
    {/* ... */}
  );
}

I am using await directly in the component because my Next.js demo app uses new App Router built on React Server Components. Now, let's take a look if our data is loading properly.

Next.js demo app - result

Looks like the data is loading properly!

SvelteKit

Now, let's do the same with SvelteKit. This app will display the list of posts. First, we need to add our database package as a dependency in the package.json file.

"dependencies": {
  // ...
  "database": "workspace:*"
}

After adding this dependency remember to run the dependency installation command, for example pnpm install, again to make sure it's available to our app. Next to our +page.svelte file let's create a new one - +page.server.ts, which will contain the code to get data from the database, and by naming it *.server.ts we are making sure that it will always run on the server.

import { db, posts, type Post } from 'database';

export async function load({ params }) {
  const postsData: Post[] = await db.select().from(posts);

  return {
    posts: postsData,
  };
}

And of course we will add our .env file with DATABASE_URL variable and install dotenv library.

# npm
npm i dotenv --filter sveltekit-app

# pnpm
pnpm add dotenv --filter sveltekit-app
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/drizzle-turbo-demo'

In +page.svelte add code that will show the data we have loaded in load function.

<script lang="ts">
  // ...
  import type { Post } from 'database';
  export let data: { posts: Post[] };
</script>

<!-- ... -->
<div>
  {#each data.posts as post}
  <div class="mb-6 max-w-3xl">
    <div class="mb-2">
      <div class="text-3xl">{post.title}</div>
      <div>{post.userId}</div>
    </div>
    <div>{post.content}</div>
  </div>
  {/each}
</div>
<!-- ... -->
SvelteKit demo app - result

Looks like everything is working also in our SvelteKit app! The list of posts is loading properly, but we would love to see the user's name instead of an ID, so let's change this.

In the database package I will re-export drizzle-orm, but you can, of course, add it as a dependency to the project itself. Let's update the index.ts file for the database package.

// ...
export * from 'drizzle-orm';

Now, I can create a simple join in our load function. I will import re-exported eq function from our database package.

import { db, posts, users, eq } from 'database';

export interface PostWithUserData {
  id: number;
  title: string;
  content: string;
  userId: number;
  userName: string;
}

export async function load({ params }): Promise<{ posts: PostWithUserData[] }> {
  const data = await db
    .select({
      id: posts.id,
      title: posts.title,
      content: posts.content,
      userId: posts.userId,
      userName: users.name,
    })
    .from(users)
    .innerJoin(posts, eq(users.id, posts.userId));

  return {
    posts: data,
  };
}

Now, let's update our +page.svelte file to use the new data.

<script lang="ts">
  // ...
  import type { PostWithUserData } from './+page.server';

  export let data: { posts: PostWithUserData[] };
</script>

<!-- ... -->
<div>
  {#each data.posts as post}
  <div class="mb-6 max-w-3xl">
    <div class="mb-2">
      <div class="text-3xl">{post.title}</div>
      <div>{post.userName}</div>
    </div>
    <div>{post.content}</div>
  </div>
  {/each}
</div>
<!-- ... -->
Next.js demo app - updated result

Conclusion

In summary, it's possible to easily create a shared database package with Turborepo and DrizzleORM that contains all database-related definitions and scripts. I hope you've gained useful insights about using Turborepo and DrizzleORM through this post.

If you have any questions or feedback, feel free to reach out to me on GitHub, Twitter/X or LinkedIn!

Happy coding!

Source code

Source code for this post is available on GitHub: drizzleorm-turborepo-demo.