How I migrated from Prisma to DrizzleORM with absolutely no hassle and zero downtime

Angelelz
Drizzle Stories
Published in
4 min readOct 25, 2023

--

If you are reading this, I take it that you looking into moving from Prisma to Drizzle, I didn’t go over why I did it, but if you want me to, let me know

Chad Drizzle vs little prisma

I work at a small service company and maintain our in-house time-tracking/quoting/AI/vehicle-inspection software. I started this as a side project from scratch. It doesn’t even have a name; we just call it The app.

The app is a Typescript monorepo with a database package, a nextjs application for the web/backend and a react native with expo application for mobile. The nextjs app serves as a backend for the web and the mobile apps. I love the architecture. This is my pet project, my baby. I started it with Prisma as my ORM due to the super easy and nice API to access my DB data. But this was my first time using it so I cautiously put it inside a DataSource class that implemented an interface that described how I wanted my data (similar to the repository pattern).

This is your step 0. Don’t let anything in you application import from Prisma, only the DataSource class. If you want to be extra fancy, you could even use dependency injection when instantiating the DataSource class, so it won’t need to know about Prisma. But that’s a story for another day.

// file: src/packages/utils/types/DataSourceBase.ts
export interface DataSourceBase {
getAllUsers(): Promise<AppUser[]>;

changePassword(userId: string, password: string): Promise<User | null>;

getByIdWithHours(userId: string, take: number): Promise<UserWithHours>;

getById(userId: string): Promise<AppUser | null>;
}

// file: src/packages/prisma/DataSource.ts
export class DataSource implements DataSourceBase {
// Prisma client was injected, but we're not going to talk about it here
// This is not how it was in the codebase
constructor(private database: PrismaClient) {}

getAllUsers() {
return this.database.user.findMany({
select: {
...appUserColumns,
},
});
}

changePassword(userId: string, password: string) {
return this.database.user.update({
where: {
id: userId,
},
data: {
password,
},
});
}

... // Rest of methods, you get the idea
}

This was my setup before I even knew anything about Drizzle. Call me crazy, but I just didn’t want my app to be tightly coupled with my database layer.

Your step 1 will be to create a new package in your monorepo (or folder in your codebase) and create a new DataSource clase that implements your DataSourceBase interface but this time with Drizzle.

This was the most fun I’ve had in a long time. With Drizzle, you don’t find yourself reading the documentation too much, but reading about how to do stuff in SQL. This makes you think about performance and optimization more than how you implement stuff with the ORM. Anyway, this is what I came up with:

// file src/packages/drizzle/DataSource.ts
export class DataSource implements DataSourceBase {
constructor(private database: Drizzle) {}

getAllUsers(): Promise<AppUser[]> {
return this.database
.select(appUserColumns)
.from(user);
}

changePassword(userId: string, password: string): Promise<User | null> {
return this.database.transaction(async (tx) => {
await tx
.update(user)
.set({
password,
})
.where(eq(user.id, userId));

const dbUser = await tx
.select()
.from(user)
.where(eq(user.id, this.userId));

if (!dbUser[0]) {
return null;
}

return dbUser[0];
});
}
... // Rest of the methods
}

Depending on the size of your codebase, this might take some time. In our codebase we actually have several DataSource clases for different tables/features. For example, the above example is actually called UserDataSource. So we started creating our DataSource classes without them being called anywhere, we ran the same integration tests that we ran on the Prisma ones and when we felt ready, we started migrating.

Your Step 2 will be to start slowly migrating all the import { DataSource } from "@company/prisma"; to import { DataSource } from "@company/drizzle". We had a little sed command that we would run by folder. We took it patiently, migrated a folder, tested, waited, and then moved to the next.

For example, our page.ts file looks like this:

import { cookies } from "next/headers";
import LandingPage from "@/components/server/landing";
import { getJWTUser } from "@/server/auth-user";
import { type TypeOf } from "zod";

import { dataSource } from "@company/drizzle/db";
import { type jwtUserSchema } from "@company/utils/validation";

import { HoursSummary, Timeclock } from "./client-components";

export default async function Home() {
const user = await getJWTUser(cookies());
// eslint-disable-next-line
// @ts-ignore
return user ? <UserHomePage user={user} /> : <LandingPage />;
}

async function UserHomePage({ user }: { user: TypeOf<typeof jwtUserSchema> }) {
const userInfo = await dataSoure.UserDataSource.getUserInfo(user);
return (
<>
<h1>{user.username.toLocaleUpperCase()}</h1>
<section className="main-content max-w-sm">
<HoursSummary userInfo={userInfo} />
<Timeclock userInfo={userInfo} />
</section>
</>
);
}

Step 3: ???

Step 4: Profit!

We’ve been running with this setup for some time and we couldn’t be happier. One of the reasons we moved to Drizzle is because we were going to move our database to the cloud. Since then, Drizzle has given us the control to optimize our performance and row reads.

I’ve also become a Drizzle advocate. I’ve contributed PRs to the project and hang out in the Discord server helping as much as I can with the many new Drizzle users. Come join us, and I guarantee you’ll find me there.

--

--

Angelelz
Drizzle Stories

Full stack JavaScript/Typescript freelancer 🚀. Drizzle-orm advocate.