t3-stack popularly known as tRPC is a lightweight library tailored specifically for building end-to-end typesafe APIs without the need for defined schemas or extra dependencies for code generation. This article will teach you how to use Yarn Workspaces to set up tRPC with React.js, Express, Prisma, PostgreSQL, and Node.js.

At the time of writing this article, @tRPC/server now has adaptors for AWS Lambda, Express, and Fastify. Whereas the @tRPC/client can be used to create Vanilla, React.js, and Next.js clients.

In addition, the developer communities are working hard to create tRPC extensions for other frameworks, like Vue.js, Svelte, and many more.

tRPC API with React.js, Express, Prisma, Postgres, and Node.js Series:

  1. How to Setup tRPC API with Prisma, PostgreSQL, Node & React
  2. tRPC API with PostgreSQL, Prisma, Redis, & Node.js: JWT Authentication

Read more articles:

How to Setup tRPC API with Prisma, PostgreSQL, Node & React

Prerequisites

Before you begin, you should:

  • Have Node.js installed on your system
  • Be comfortable with Node.js and Express
  • Be comfortable with React.js

What is tRPC?

tRPC is a lightweight toolkit that allows developers to use the power of TypeScript to build end-to-end typesafe APIs with Express, Fastify, AWS Lambda, and more without the need to create schemas or install extra dependencies for code generation.

Whether you are new to this web development ecosystem or you are a veteran player, you may have noticed that GraphQL is the dominant library for building type-safety APIs using a query language.

GraphQL is amazing and solved most of our miseries, however, it doesn’t take full advantage of TypeScript to write type-safety APIs.

For this reason, tRPC was introduced to use the full power of TypeScript to build full-stack type-safety applications.

Setup PostgreSQL and Redis with Docker

PostgreSQL is a powerful, free, and open-source relational database management system used as the primary data store for web, and mobile applications.

Running PostgreSQL with Docker and docker-compose gives you the ability to easily run and maintain it, especially in a development environment.

To simply run the containers using the Postgres and Redis Docker images we can create a docker-compose.yml file with the required configurations.

First, create a project folder named trpc-prisma-react and open it with your preferred IDE or text editor.

$ mkdir trpc-prisma-react

Next, create a docker-compose.yml in the project directory and add the following configurations to set up the Redis and PostgreSQL containers.

docker-compose.yml


version: '3'
services:
  postgres:
    image: postgres
    container_name: postgres
    ports:
      - '6500:5432'
    restart: always
    env_file:
      - ./.env
    volumes:
      - postgres-db:/var/lib/postgresql/data
  redis:
    image: redis:latest
    container_name: redis
    ports:
      - '6379:6379'
    volumes:
      - redis:/data
volumes:
  postgres-db:
  redis:

Create a .env file and add the following environment variables to send in the Postgres user, database name, and password.

.env


DATABASE_PORT=6500
POSTGRES_PASSWORD=password123
POSTGRES_USER=postgres
POSTGRES_DB=trpc_prisma
POSTGRES_HOST=postgres
POSTGRES_HOSTNAME=127.0.0.1

After this quick setup, start the containers with this command:

docker-compose up -d

Execute this command to stop the containers:

docker-compose down

Use Yarn Workspaces to Setup Monolithic Repository

First, let’s build a monorepo for the tRPC project with Yarn. For those who are new to the web and mobile development ecosystem, Yarn is simply a package manager developed by the guys at Facebook. It has a tool called Yarn workspaces, for organizing a project codebase into a monolithic repository.

With the explanation above, create a package.json file in the root workspace:

touch package.json

Next, add the following code to the newly-created package.json file:


{
  "name": "trpc-node-react",
  "private": "true",
  "scripts": {},
  "workspaces": [
    "packages/*"
  ]
}

Workspaces are not meant to be published so we need to set the private property to true in the package.json file to avoid publishing the root workspace by mistake.

Next, we used wildcard (*) in the “workspaces” array for Yarn to recognize the folders in the “packages” directory as packages.

Don’t be anxious, we will create the “packages” folder in a moment.

Create a Script to Start the tRPC client and server

Now let’s install the concurrently and wsrun packages help us start the tRPC client and server in parallel.

Using this approach will save us a lot of time since we do not have to manually change directories in the terminal to run the start scripts of the tRPC client and server.

Install the concurrently and wsrun packages with this command:


yarn add -W -D concurrently wsrun

The “-W” flag tells Yarn to add the dependencies to the workspace itself. Whereas the “-D” flag tells Yarn to add the packages as dev-dependencies.

Update the package.json file in the root workspace with the following:

package.json


{
  "name": "trpc-prisma-react",
  "private": "true",
  "scripts": {
    "start": "concurrently \"wsrun --parallel start\""
  },
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
    "concurrently": "^7.3.0",
    "wsrun": "^5.2.4"
  }
}


The start script in the above will help us execute the start scripts in the tRPC client and server in parallel.

Since we will be pushing the project to GitHub, create a .gitignore file in the root workspace and exclude the node_modules.

.gitignore


node_modules

Creating the tRPC Node.js Server in Express

We are now ready to set up the tRPC Express Node.js server.

First, create a “server” folder in the “packages” folder then open the integrated terminal and change the directory into it cd packages/server .

cd packages/server

Now run the command below to initialize a Node.js TypeScript project with Yarn:


yarn init -y && yarn add -D typescript && npx tsc --init 

Next, open the newly-created “tsconfig.json” file and replace its content with the following configurations:

packages/server/tsconfig.json


{
  "compilerOptions": {
    "target": "es2018",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "strictPropertyInitialization": false,
    "skipLibCheck": true
  }
}

  • experimentalDecorators – allows you to set experimental support for decorators.
  • emitDecoratorMetadata – allows you to enable experimental support for emitting type metadata for the decorators.
  • strictPropertyInitialization – needed to prevent Typescript from raising errors when we declare the Typegoose class attributes without initializing them.

Add the dependencies required to set up the tRPC server with Express:


yarn add @trpc/server@next cors dotenv express @prisma/client redis && yarn add -D prisma @types/cors @types/express @types/node morgan @types/morgan ts-node-dev

  • @trpc/server – has tools for building the tRPC endpoints and routers
  • cors – needed to configure the tRPC Express server to accept requests from cross-origin domains.
  • dotenv – loads the environment variables from a configuration file into the Node.js environment.
  • express – a lightweight Node.js framework for building web and mobile applications.
  • @prisma/client – the query builder for Prisma
  • prisma – the CLI for running Prisma commands
  • redis – a JavaScript library for accessing and mutating the Redis database
  • ts-node-dev – to hot-reload the tRPC Express server upon every file change.
  • morgan – an HTTP logger middleware

The security of the tRPC application is very important and it’s always a best practice to store sensitive data in an environment variables file. Create a packages/server/.env file and add the following credentials:


NODE_ENV=development

ORIGIN=http://127.0.0.1:3000

DATABASE_URL="postgresql://postgres:password123@localhost:6500/trpc_prisma?schema=public"

Open the packages/server/package.json file and add the start script to help us start the tRPC Express server.

packages/server/package.json


{
"scripts": {
    "start": "ts-node-dev --respawn --transpile-only src/app.ts"
  }
}

Within the packages/server folder, create an “src” folder. Next, create two folders named “config” and “utils” in the “src” directory.

Next, create a default.ts file in the “config” directory to enable us to load and export the environment variables.

packages/server/src/config/default.ts


import path from 'path';
require('dotenv').config({ path: path.join(__dirname, '../../.env') });

const customConfig: { port: number; origin: string; dbUri: string } = {
  port: 8000,
  origin: process.env.ORIGIN as unknown as string,

  dbUri: process.env.DATABASE_URL as unknown as string,
};

export default customConfig;


Connect the tRPC Server to PostgreSQL and Redis

Now that we have the Redis and PostgreSQL servers running in the Docker containers, let’s define some utility functions to connect them to the tRPC server.

packages/server/src/utils/prisma.ts


import { PrismaClient } from '@prisma/client';

declare global {
  var prisma: PrismaClient | undefined;
}

export const prisma =
  global.prisma || new PrismaClient({ log: ['query', 'info'] });

if (process.env.NODE_ENV !== 'production') {
  global.prisma = prisma;
}

async function connectDB() {
  try {
    await prisma.$connect();
    console.log('? Database connected successfully');
  } catch (error) {
    console.log(error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}

export default connectDB;

packages/server/src/utils/connectRedis.ts


import { createClient } from 'redis';

const redisUrl = `redis://localhost:6379`;
const redisClient = createClient({
  url: redisUrl,
});

const connectRedis = async () => {
  try {
    await redisClient.connect();
    console.log('? Redis client connected...');
    redisClient.set(
      'tRPC',
      '??Welcome to tRPC with React.js, Express and Typescript!'
    );
  } catch (err: any) {
    console.log(err.message);
    process.exit(1);
  }
};

connectRedis();

redisClient.on('error', (err) => console.log(err));

export default redisClient;

In the above, we created a function to connect to the Redis server and evoked the redisClient.set() function returned by calling the createClient() function to add a message to the Redis database with a “tRPC” key.

Later, we will request the message from the Redis database and send it to the tRPC React.js client.

Starting the tRPC Express Server

Creating the tRPC Context

Now let’s create a context function that will be called for every incoming request and the results will be passed as contextual data to all the resolvers.

packages/server/src/app.ts


import path from 'path';
import dotenv from 'dotenv';
import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
import { inferAsyncReturnType, initTRPC, TRPCError } from "@trpc/server";
import * as trpcExpress from '@trpc/server/adapters/express';
import redisClient from './utils/connectRedis';
import customConfig from './config/default';
import connectDB from './utils/prisma';

dotenv.config({ path: path.join(__dirname, './.env') });

const createContext = ({
  req,
  res,
}: trpcExpress.CreateExpressContextOptions) => ({ req, res });

export type Context = inferAsyncReturnType<typeof createContext>;
// [...]

Creating the tRPC Router

Next, let’s use the “router()” function provided by tRPC to create a router to manage the tRPC endpoints. This will allow us to add:

  • Query endpoints – for requesting data
  • Mutation endpoints – to perform the Create, Update, and Delete operations.
  • Subscription endpoints – used to subscribe to data over WebSockets.

packages/server/src/app.ts


// [...]
const t = initTRPC.context<Context>().create();

const appRouter = t.router({
  sayHello: t.procedure.query(async () => {
    const message = await redisClient.get("tRPC");
    return { message };
  }),
});

export type AppRouter = typeof appRouter;

Let’s evaluate the code above:

First, we initialized the tRPC server by evoking the .context<Context>() and .create() methods. Then, we created the tRPC router and added a sayHello procedure call. The sayHello query will be evoked by the tRPC client to return the message stored in the Redis database.

The appRouter returned by evoking t.router() function maintains all the endpoints, including what the routes accept as inputs, and their outputs.

Next, we exported the AppRouter type from the packages/server/src/app.ts file. This will allow us to install the tRPC server as a package in the tRPC client in order for the React client to know the different queries, mutations, and subscriptions available on the tRPC server.

Configuring the Express tRPC Server

packages/server/src/app.ts


// [...]
const app = express();
if (process.env.NODE_ENV !== "production") app.use(morgan("dev"));

app.use(
  cors({
    origin: [customConfig.origin, "http://localhost:3000"],
    credentials: true,
  })
);
app.use(
  "/api/trpc",
  trpcExpress.createExpressMiddleware({
    router: appRouter,
    createContext,
  })
);

const port = customConfig.port;
app.listen(port, () => {
  console.log(`🚀 Server listening on port ${port}`);

  // CONNECT DB
  connectDB();
});

Complete Express tRPC Server Code

packages/server/src/app.ts


import path from "path";
import dotenv from "dotenv";
import express from "express";
import morgan from "morgan";
import cors from "cors";
import { inferAsyncReturnType, initTRPC } from "@trpc/server";
import * as trpcExpress from "@trpc/server/adapters/express";
import redisClient from "./utils/connectRedis";
import customConfig from "./config/default";
import connectDB from "./utils/prisma";

dotenv.config({ path: path.join(__dirname, "./.env") });

const createContext = ({
  req,
  res,
}: trpcExpress.CreateExpressContextOptions) => ({ req, res });

export type Context = inferAsyncReturnType<typeof createContext>;

const t = initTRPC.context<Context>().create();

const appRouter = t.router({
  sayHello: t.procedure.query(async () => {
    const message = await redisClient.get("tRPC");
    return { message };
  }),
});

export type AppRouter = typeof appRouter;

const app = express();
if (process.env.NODE_ENV !== "production") app.use(morgan("dev"));

app.use(
  cors({
    origin: [customConfig.origin, "http://localhost:3000"],
    credentials: true,
  })
);
app.use(
  "/api/trpc",
  trpcExpress.createExpressMiddleware({
    router: appRouter,
    createContext,
  })
);

const port = customConfig.port;
app.listen(port, () => {
  console.log(`🚀 Server listening on port ${port}`);

  // CONNECT DB
  connectDB();
});

Turning the tRPC Express Server into a Library

To convert the tRPC server into a library, we need to add "main": "src/app.ts" to the packages/server/package.json file. This will allow us to have access to the AppRouter type when we install the tRPC server in the tRPC React.js client.

packages/server/package.json

setup express nodejs trpc server with prisma

packages/server/package.json


{
  "name": "server",
  "version": "1.0.0",
  "main": "src/app.ts",
  "license": "MIT",
  "scripts": {
    "start": "ts-node-dev --respawn --transpile-only src/app.ts",
    "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
    "db:push": "npx prisma db push"
  },
  "devDependencies": {
    "@types/cors": "^2.8.12",
    "@types/express": "^4.17.14",
    "@types/morgan": "^1.9.3",
    "morgan": "^1.10.0",
    "prisma": "^4.5.0",
    "ts-node-dev": "^2.0.0",
    "typescript": "^4.8.4"
  },
  "dependencies": {
    "@prisma/client": "^4.5.0",
    "@trpc/server": "^10.0.0-proxy-beta.26",
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "redis": "^4.3.1"
  }
}

Database Migration with Prisma

There are other ORMs for Node.js but we are going to use Prisma to query and mutate the database.

To get started, run the following code to initialize a new Prisma project in the packages/server folder.

Change the directory into the packages/server folder and run this command:


npx prisma init

The above command will generate a new packages/server/prisma folder containing a schema.prisma file. Also, when you open the environment variables file in the server folder, you should see a DATABASE_URL variable added by the Prisma CLI.

Now replace the content of the packages/server/prisma/schema.prisma file with the following code:

packages/server/prisma/schema.prisma


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}


model User{
  @@map(name: "users")

  id String  @id @default(uuid())
  name String  @db.VarChar(255)
  email String @unique
  photo String? @default("default.png")
  verified Boolean? @default(false) 
  
  password String
  role RoleEnumType? @default(user)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  provider String?
}

enum RoleEnumType {
  user
  admin
}

Quite a lot going on in the above, let’s break it down:

  • First, we created a User model and used the @@map(name: "users") attribute to change the table name to users .
  • Then we defined the attributes needed to create a new user in the database.
  • The @default(uuid()) attribute sets a default UUID value for the ID field in the database.

With the above explanation, add the following scripts to the packages/server/package.json file.


{
"scripts": {
    "start": "ts-node-dev --respawn --transpile-only src/app.ts",
    "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
    "db:push": "npx prisma db push"
  },
}
  • db:migrate – create a new Prisma migration file and generate the TypeScript types based on the defined model.
  • db:push – push the changes to the PostgreSQL database and ensures that the database is in-sync with the schema.

Add the PostgreSQL credentials defined in the .env file to the DATABASE_URL added by the Prisma CLI.


DATABASE_URL="postgresql://postgres:password123@localhost:6500/trpc_prisma?schema=public"

Now to create the users table in the PostgreSQL database, run the following command:


yarn db:migrate && yarn db:push

When you log into the PostgreSQL database in pgAdmin with the credentials defined in the .env , you should see the users table with the following properties.

checking the prisma model attributes in postgresql using pgadmin

Creating the tRPC Client in React.js

First, open the integrated terminal and change the directory into the “packages” folder to enable us to create a boilerplate React.js app with the Vite scaffolding tool.

Run this command to scaffold a new Vite project with Yarn:


yarn create vite

The above command will install and execute the Vite executable from the remote npm repository and you will be presented with prompts for a number of features like:

  • The project name
  • The type of framework
  • TypeScript support

After the script has been executed, the tool will prompt you to enter a project name:


$ yarn create vite
yarn create v1.22.19
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Installed "create-vite@3.0.0" with binaries:
      - create-vite
      - cva
? Project name: » client

Type “client” as the project name and press the Enter key

After pressing the Enter key, you will be prompted to select a framework


? Select a framework: » - Use arrow-keys. Return to submit.
    vanilla
    vue
>   react
    preact
    lit
    svelte

After using the arrow keys to select the framework, press the Enter key again and Vite will prompt you to choose the language type.


? Select a variant: » - Use arrow-keys. Return to submit.
    react
>   react-ts

Select react-ts from the list and press the Enter key. Vite will then generate the project and instruct you to install the dependencies.


Scaffolding project in C:\Users\zidda\Desktop\Learn\Blog\tRPC\trpc-prisma-react\testing\client...

Done. Now run:

  cd client
  yarn
  yarn dev

Done in 544.96s.

Change the directory into the project folder as directed and run yarn to install the dependencies.

Next, open the packages/client/package.json file and change the dev script to start. Also, add the port flag to start the dev server on port 3000.

packages/client/package.json


{
"scripts": {
    "start": "vite --port 3000",
    "build": "tsc && vite build",
    "preview": "vite preview"
  }
}

We are now ready to install the tRPC server as a library in the React client. In your terminal, ensure that you are in the client folder and run the command below to install the tRPC server library.


yarn add server@1.0.0

server – is the property name in the packages/server/package.json file

@1.0.0 – is the corresponding version specified in it.

Next, run the following command to install the dependencies needed to set up the tRPC client:


yarn add @trpc/client@next @trpc/server@next @trpc/react-query@next @tanstack/react-query @tanstack/react-query-devtools

Creating the tRPC Client

Now let’s use the createTRPCReact() function provided by @trpc/react-query binding to generate the React Query hooks based on the queries, mutations, and subscriptions defined on the tRPC server.

packages/client/src/utils/trpc.ts


import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "server";

export const trpc = createTRPCReact<AppRouter>();

Testing the tRPC API Endpoint

Now let’s configure the React.js client with tRPC and React Query before making our first tRPC request to our API server.

packages/client/src/App.tsx


import "./global.css";
import { useState } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { getFetch } from "@trpc/client";
import { loggerLink } from "@trpc/client/links/loggerLink";
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
import { trpc } from "./utils/trpc";

function AppContent() {
  const hello = trpc.sayHello.useQuery();
  return <main className="p-2">{JSON.stringify(hello.data, null, 2)}</main>;
}

function App() {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 5 * 1000,
          },
        },
      })
  );

  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        loggerLink(),
        httpBatchLink({
          url: "http://localhost:8000/api/trpc",
          fetch: async (input, init?) => {
            const fetch = getFetch();
            return fetch(input, {
              ...init,
              credentials: "include",
            });
          },
        }),
      ],
    })
  );
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <AppContent />
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

export default App;

With the above configurations, we are now ready to start the tRPC client and server. Open your terminal and change the directory into the root Yarn workspace and run yarn start to start both the tRPC client and server.

Enter the tRPC client URL http://127.0.0.1:3000/ in the browser and you should see the message we stored in the Redis database.

testing trpc client and server with react and expressjs

Adding tailwindCss to the React.js tRPC Client

In this section, follow the steps below to add tailwindCss to the React.js tRPC client.

Step 1: Install tailwindCss and its dependencies

Open the integrated terminal and change the directory to the packages/client folder. Run this command to install the tailwindCss library and its peer dependencies via Yarn.


yarn add -D tailwindcss postcss autoprefixer

Next, run the init command to generate the tailwind.config.js and postcss.config.js configuration files.


npx tailwindcss init -p

Step 2: Configure Template Paths

Next, open the tailwind.config.js file and add the path to the React template files. You can also include your custom colors and fonts.

packages/client/tailwind.config.js


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {
      colors: {
        'ct-dark-600': '#222',
        'ct-dark-200': '#e5e7eb',
        'ct-dark-100': '#f5f6f7',
        'ct-blue-600': '#2363eb',
        'ct-yellow-600': '#f9d13e',
      },
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },
  plugins: [],
};

Step 3: Adding the tailwindCss directives

Now create a packages/client/src/global.css file and add the TailwindCSS directives.

packages/client/src/global.css


@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  html {
    font-family: 'Poppins', sans-serif;
  }
}

Step 2: Importing the CSS file

We are now ready to start using the tailwindCss classes but before that let’s import the packages/client/src/global.css file into the App.tsx file.

packages/client/src/App.tsx


import './global.css';
// [...] other imports

Next, replace the content of the AppContent() component in the packages/client/src/App.tsx file with the following:

packages/client/src/App.tsx


function AppContent() {
  const hello = trpc.sayHello.useQuery();
  return (
    <div className="container mx-auto bg-ct-dark-200 rounded-xl shadow border p-8 m-10">
      <p className="text-3xl text-gray-700 font-bold mb-5">Welcome!</p>
      <p className="text-ct-blue-600 text-lg">{hello.data?.message}</p>
    </div>
  );
}

You should now see the following output in the browser:

added tailwindcss to the tRPC client

Conclusion

With this tRPC client and server example, you’ve learned how to set up tRPC with React.js, PostgreSQL, Prisma, Express, and Redis.

You can find the source code of the tRPC client and server on my GitHub