Shared database schema with DrizzleORM and Turborepo
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
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
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
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.
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;
};
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>;
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 fromdrizzle-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 thedotenv
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 ifDATABASE_URL
is not provided. - Then, we create a
client
object using thepostgres
library. The only thing we need to provide here is ourconnectionString
. - 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.
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
We can see that we have successfully applied the change with the migration.
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.
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'
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.
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>
<!-- ... -->
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>
<!-- ... -->
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.
Happy coding!
Source code for this post is available on GitHub: drizzleorm-turborepo-demo.