DEV Community

Cover image for Setting Up Drizzle & Postgres with tRPC and Next.js App
Rakesh Potnuru
Rakesh Potnuru

Posted on • Originally published at blog.itsrakesh.com

Setting Up Drizzle & Postgres with tRPC and Next.js App

Published from Publish Studio

In this tutorial, let's learn how to connect a Postgres database to a tRPC express backend using Drizzle ORM. I have also created a simple frontend for our finance tracker application. You can copy frontend code from the repo here.

finance tracker

This is part 2, read part 1 here: Let's Build a Full-Stack App with tRPC and Next.js 14

Backend

If you don't have Postgres installed locally, please do or you can also use a hosted database.

Once you have Postgres ready, add DATABASE_URL to your .env:

DATABASE_URL=postgres://postgres:password@localhost:5432/myDB
Enter fullscreen mode Exit fullscreen mode

Setting up db with drizzle

To set up drizzle, start off by installing these packages:

yarn add drizzle-orm pg dotenv
yarn add -D drizzle-kit tsx @types/pg
Enter fullscreen mode Exit fullscreen mode

Now, all you have to do is connect drizzle to the DB. To do that, create src/utils/db.ts file and configure drizzle:

import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
const { Pool } = pg;

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === "production",
});

export const db = drizzle(pool);
Enter fullscreen mode Exit fullscreen mode

That's it! Our db setup is ready. We can now create tables and interact with our db using drizzle ORM.

Create the first module

Regarding the project structure, there are mainly two types:

  1. Modules: Divide features into different modules and keep all related files together. Popular frameworks like NestJs and Angular use this structure.
.
└── feature/
    ├── feature.controller.ts
    ├── feature.routes.ts
    ├── feature.schema.ts
    └── feature.service.ts
Enter fullscreen mode Exit fullscreen mode
  1. Separate folders based on the purpose:
.
├── controllers/
│   ├── feature1.controller.ts
│   └── feature2.controller.ts
├── services/
│   ├── feature1.service.ts
│   └── feature2.service.ts
└── models/
    ├── feature1.model.ts
    └── feature2.model.ts
Enter fullscreen mode Exit fullscreen mode

I personally prefer modules because it just makes sense (plz stop using 2nd one).

Now, let's create our first module called transaction. This is our core feature. Start by creating src/modules/transaction/transaction.schema.ts file. This is where we define transaction schema using drizzle.

The great thing about using drizzle to write schemas is it lets us use typescript. So you don't have to learn a new syntax and ensure type safety for your schemas.

To record a transaction (txn), the most basic things we need are:

  • txn amount
  • txn type - credit or debit
  • description - a simple note to refer to later
  • tag - a category like shopping/travel/food, and so on.

First, let's create enums for txn type and tag:

import {
  pgEnum,
} from "drizzle-orm/pg-core";

export const txnTypeEnum = pgEnum("txnType", ["Incoming", "Outgoing"]);
export const tagEnum = pgEnum("tag", [
  "Food",
  "Travel",
  "Shopping",
  "Investment",
  "Salary",
  "Bill",
  "Others",
]);
Enter fullscreen mode Exit fullscreen mode

Then, let's create the schema:

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

export const transactions = pgTable("transactions", {
  id: serial("id").primaryKey(),
  amount: integer("amount").notNull(),
  txnType: txnTypeEnum("txn_type").notNull(),
  summary: text("summary"),
  tag: tagEnum("tag").default("Others"),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at")
    .defaultNow()
    .$onUpdate(() => new Date()),
});
Enter fullscreen mode Exit fullscreen mode

As you can see, we simply wrote typescript code and created a table!

Run migrations

One final step before we can start interacting with our db is to apply changes to our database so that all the tables will be created. To do that we have to run migrations. Drizzle has this amazing tool called drizzle-kit which handles migrations for us, so all we have to do is run a command.

Before doing that we have to create a file called drizzle.config.ts in the project root, which includes all the information about the database and schemas.

import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/**/*.schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
    ssl: process.env.NODE_ENV === "production",
  },
});
Enter fullscreen mode Exit fullscreen mode

With that ready, run the below command:

yarn dlx drizzle-kit push
Enter fullscreen mode Exit fullscreen mode

That's it! Now we can start interacting with db and write our business logic.

Business logic

Let's add logic to add new transactions.

If you already don't know:

  • Service - where we interact with DB and write most of the business logic
  • Controller - handle request/response

Create transaction/transaction.service.ts and write logic to add new transactions to db:

import { TRPCError } from "@trpc/server";
import { db } from "../../utils/db";
import { transactions } from "./transaction.schema";

export default class TransactionService {
  async createTransaction(data: typeof transactions.$inferInsert) {
    try {
      return await db.insert(transactions).values(data).returning();
    } catch (error) {
      console.log(error);

      throw new TRPCError({
        code: "INTERNAL_SERVER_ERROR",
        message: "Failed to create transaction",
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Another benefit of using drizzle ORM is it provides type definitions for different CRUD methods like $inferInsert, $inferSelect so there is no need to define the types again. Here, by using typeof transactions.$inferInsert we don't have to provide values for fields like primary key, and fields with default values like createdAt, and updatedAt, so typescript won't throw an error.

Drizzle also has extensions like drizzle-zod which can be used to generate zod schemas. Another headache was prevented by drizzle 🫡. So open transaction.schema.ts and create zod schema for insert operation:

import { createInsertSchema } from "drizzle-zod";

export const insertUserSchema = createInsertSchema(transactions).omit({
  id: true,
  createdAt: true,
  updatedAt: true,
});
Enter fullscreen mode Exit fullscreen mode

Let's use this in the controller, create transaction/transaction.controller.ts:

export default class TransactionController extends TransactionService {
  async createTransactionHandler(data: typeof transactions.$inferInsert) {
    return await super.createTransaction(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, all that remains is to expose this controller through an endpoint. For that, create transaction/transaction.routes.ts. Since we are using tRPC, to create an endpoint, we have to define a procedure:

import { publicProcedure, router } from "../../trpc";
import TransactionController from "./transaction.controller";
import { insertUserSchema } from "./transaction.schema";

const transactionRouter = router({
  create: publicProcedure
    .input(insertUserSchema)
    .mutation(({ input }) =>
      new TransactionController().createTransactionHandler(input)
    ),
});

export default transactionRouter;
Enter fullscreen mode Exit fullscreen mode

If you remember from part 1, we created a reusable router that can be used to group procedures and publicProcedure which creates an endpoint.

Finally, open src/routes.ts and use the above transactionRouter:

import transactionRouter from "./modules/transaction/transaction.routes";
import { router } from "./trpc";

const appRouter = router({
  transaction: transactionRouter,
});

export default appRouter;
Enter fullscreen mode Exit fullscreen mode

That's it! The backend is ready. This is the final backend structure:

.
├── README.md
├── drizzle
│   ├── 0000_true_junta.sql
│   └── meta
│       ├── 0000_snapshot.json
│       └── _journal.json
├── drizzle.config.ts
├── package.json
├── src/
│   ├── index.ts
│   ├── modules/
│   │   └── transaction/
│   │       ├── transaction.controller.ts
│   │       ├── transaction.routes.ts
│   │       ├── transaction.schema.ts
│   │       └── transaction.service.ts
│   ├── routes.ts
│   ├── trpc.ts
│   └── utils/
│       ├── db.ts
│       └── migrate.ts
├── tsconfig.json
└── yarn.lock
Enter fullscreen mode Exit fullscreen mode

Challenge for you

Before proceeding to frontend integration, as a challenge, create an endpoint for getting all transactions.

Frontend

It's time to integrate the created endpoints in our frontend. Since this is not a frontend tutorial, I'll let you just copy the code from the repo.

All I've changed is:

  • Set up shadcn/ui
  • Change src/components/modules/dashboard/index.tsx

Also, as you observe, I'm using a modules-like structure here too. If you also like this structure, you can learn more from my previous projects Publish Studio and My One Post

In part 1, we queried data using built-in tRPC react-query.

...
  const { data } = trpc.test.useQuery();

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      {data}
    </main>
  );
...
Enter fullscreen mode Exit fullscreen mode

So, if you already know react-query, there's isn't much to learn except with tRPC we don't have to create queryFn or mutationFn because we directly call backend methods.

This is how mutations are used:

...
  const { mutateAsync: createTxn, isLoading: isCreating } =
    trpc.transaction.create.useMutation({
      onSuccess: async () => {
        form.reset();
        await utils.transaction.getAll.invalidate();
      },
    });

  const addTransaction = async (data: z.infer<typeof formSchema>) => {
    try {
      await createTxn(data);
    } catch (error) {
      console.error(error);
    }
  };
...
Enter fullscreen mode Exit fullscreen mode

See In Action

completed tutorial


I hope you like this tutorial. Feel free to extend the functionality. In the next article, I'll share how to add authentication.


Project source code can be found here.


Follow me for more 🚀. Socials

Top comments (0)