tRPC aka t3-stack is a light library for building end-to-end typesafe APIs for Next.js and Node apps without writing schemas or installing libraries for code generation. This article will teach you how to set up tRPC with React.js, Express, and Node.js using Yarn Workspaces.

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

  1. Build tRPC API with React.js, Node.js & MongoDB: Project Setup
  2. Build tRPC API with React.js & Node.js: Access and Refresh Tokens
  3. Full-Stack App tRPC, React.js, & Node.js: JWT Authentication

Read more articles:

Build tRPC API with React.js, Node.js & MongoDB Project Setup

What is tRPC?

tRPC is a tool that leverages the full power of modern TypeScript to build end-to-end type-safety APIs with Node.js and Next.js without defining schemas. It allows us to share the Typescript types directly between the backend and the frontend without relying on code generation.

Currently, GraphQL is the go-to library for building type-safety APIs to solve some of the downsides of RESTful APIs. However, since GraphQL is a query language, it doesn’t take full advantage of TypeScript to implement type-safety APIs.

This is where tRPC comes in, this tool uses the full power of Typescript to create a type-safe client and server by only using inference

React.js and Axios User Registratio...
React.js and Axios User Registration and Email Verification

Setup MongoDB and Redis with Docker-compose

MongoDB is an open-source NoSQL database management program that uses JSON-like documents called BSON with optional schemas to store data.

The quickest way to run the MongoDB database server on a machine is to use a Docker container so am going to assume you already have Docker installed on your machine.

By default, Docker-compose is also installed along with Docker to allow you to run multiple containers using a docker-compose.yaml file.

Create a new folder called trpc-node-react with your preferred local terminal and open the folder with your text editor of choice.

mkdir trpc-node-react

In the root directory, create a docker-compose.yml file and add the following Docker configurations:

docker-compose.yml


version: '3.8'
services:
  mongo:
    image: mongo:latest
    container_name: mongo
    env_file:
      - ./.env
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
      MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
    volumes:
      - mongo:/data/db
    ports:
      - '6000:27017'
  redis:
    image: redis:latest
    container_name: redis
    ports:
      - '6379:6379'
    volumes:
      - redis:/data
volumes:
  mongo:
  redis:


The configurations above will allow us to run both the Redis and MongoDB servers on our machine.

Now create a .env file in the root project to contain the credentials required by the Mongo Docker image to configure the MongoDB server.

.env


MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=password123
MONGO_INITDB_DATABASE=trpc_mongodb

With those configurations in place, run this command to start the Redis and MongoDB Docker containers:

docker-compose up -d

Run this command to stop the containers:

docker-compose down

Setup a Monolithic Repository with Yarn Workspaces

To begin, let’s build a monorepo using Yarn. Yarn is a package manager developed and maintained by the folks at Facebook and it comes with a tool called Yarn workspaces for organizing a project codebase using a monolithic repository aka monorepo.

Create a package.json file in the root workspace:

touch package.json

Add the following code to the newly-created package.json file:


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

Since workspaces are not meant to be published, you need to make the package.json private to avoid accidentally publishing the root workspace.

Also, we used a wildcard (*) in the “workspaces” property array to tell Yarn to include all the packages inside the “packages” folder.

Add a script to run both the tRPC client and server

Now we need a way to run both packages — the tRPC React client and the Node.js client simultaneously. For this example, we will use the concurrently and wsrun packages to run the start scripts of both packages in parallel.

Add concurrently and wsrun to the root package.json:


yarn add -W -D concurrently wsrun

Add this script to the root workspace package.json to run the start scripts of the tRPC client and server in parallel.

package.json


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

Create a .gitignore file in the root workspace and add the node_modules to exclude them from your commits.

.gitignore


node_modules

Creating the Node.js tRPC Express Server

Now it’s time to set up the tRPC Express Node.js server. Create a folder named “server” inside the “packages” folder.

Open the built-in terminal in your text editor and change the directory to the packages/server folder:

cd packages/server

Run this command to initialize a Node.js Typescript project with Yarn:


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

Now replace the content of the newly-created “tsconfig.json” file 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 – If set to “true“, experimental support for decorators will be enabled.
  • emitDecoratorMetadata – If set to “true” experimental support for emitting type metadata for the decorators will be enabled.
  • strictPropertyInitialization – If set to “false“, Typescript won’t raise errors when we declare the Typegoose class properties without initializing them in a constructor.

Install the dependencies needed to set up the tRPC server with ExpressJs:


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

  • @trpc/server – for implementing tRPC endpoints and routers
  • cors – to allow the tRPC server to accept requests from cross-origin domains.
  • dotenv – to load the environment variables into the Node.js environment.
  • express – a lightweight Node.js web application framework for building web and mobile applications.
  • mongoose – a library that uses schemas to define MongoDB models.
  • redis – a Node.js library for interacting with a Redis server
  • ts-node-dev – to hot-reload the tRPC Express server.
  • morgan – for logging HTTP requests in the terminal

Since the security of the tRPC application is really important to us, create a .env file in the packages/server folder and add the following data.


NODE_ENV=development

ORIGIN=http://localhost:3000

MONGODB_URI=mongodb://admin:password123@localhost:6000/trpc_mongodb?authSource=admin

Add the start script to the packages/server/package.json file to initialize the tRPC Express server.

packages/server/package.json


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

In the packages/server folder, create an “src” folder, and within the “src” folder create a “config” and “utils” folders.

Now create default.ts file in the “config” folder to import and export the environment variables we stored in the packages/server/.env file.

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.MONGODB_URI as unknown as string,
};

export default customConfig;


Connect the tRPC Server to Redis and MongoDB

Now that we have the Redis and MongoDB servers running in the Docker containers, let’s create some helper functions to connect them to the tRPC application.

packages/server/src/utils/connectDB.ts


import mongoose from 'mongoose';
import customConfig from '../config/default';

const dbUrl = customConfig.dbUri;

const connectDB = async () => {
  try {
    await mongoose.connect(dbUrl);
    console.log('? Database connected...');
  } catch (error: any) {
    console.log(error);
    process.exit(1);
  }
};

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 rRPC 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 code, we evoked the redisClient.set() method available on the Redis client instance to add a message to the Redis database with a “tRPC” key.

Later, we will retrieve that message from the Redis database and return it to the “tRPC” client.

Initialize the tRPC Express Server

Create the tRPC Context

Let’s create an app context that will be generated for every incoming request and the results will be propagated to all the resolvers. This will enable us to pass down contextual data to our 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 * as trpcExpress from "@trpc/server/adapters/express";
import connectDB from "./utils/connectDB";
import redisClient from "./utils/connectRedis";
import customConfig from "./config/default";
import { inferAsyncReturnType, initTRPC } from "@trpc/server";

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

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

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

Create the tRPC Router

Now let’s create a router to enable us to manage the tRPC endpoints. With the router defined, we can add:

  • Query routes – for fetching data
  • Mutation routes – for modifying data (Create, Update and Delete data)
  • Subscription routes – allows us 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;

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

Lastly, we exposed the AppRouter typescript type to enable the React client to know the different queries, mutations, and subscriptions available on the tRPC server.

AppRouter maintains all the routes, what the routes take as inputs, and what the routes return as outputs.

Start 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

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 * as trpcExpress from "@trpc/server/adapters/express";
import connectDB from "./utils/connectDB";
import redisClient from "./utils/connectRedis";
import customConfig from "./config/default";
import { inferAsyncReturnType, initTRPC } from "@trpc/server";

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();
});

Turn the tRPC Express App into a Library

Add a "main": "src/app.ts" to the packages/server/package.json to turn the tRPC server into a library. This will allow us to connect both the tRPC React.js client to the Node.js server.

packages/server/package.json

turn tRPC server to a library

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"
  },
  "dependencies": {
    "@trpc/server": "^10.0.0-proxy-beta.26",
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "mongoose": "^6.7.0",
    "redis": "^4.3.1"
  },
  "devDependencies": {
    "@types/cors": "^2.8.12",
    "@types/morgan": "^1.9.3",
    "morgan": "^1.10.0",
    "ts-node-dev": "^2.0.0",
    "typescript": "^4.8.4"
  }
}


Creating the React.js tRPC Client

First and foremost, change the directory cd into the “packages” folder to enable us to generate a basic React.js app with “create-react-app“.

Run this command to create a React.js boilerplate app in a packages/client folder.


yarn create react-app client --template typescript

Replace the content of the packages/client/tsconfig.json file with the following:


{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

Now let’s connect the tRPC server to the React client by installing the API server as a library on the client.


yarn add server@1.0.0

Where “server” is the property name in the packages/server/package.json file and the “@1.0.0” is the version provided in it. This will enable the React.js app to import the AppRouter type we exported from the tRPC server.

Next, install the dependencies required to set up the tRPC Client


yarn add @trpc/client@next @trpc/server@next @trpc/react-query@next @tanstack/react-query
  • react-query – a library for managing server state
  • @trpc/react – is a thin wrapper around React Query
  • @trpc/client – for creating the tRPC client
  • @trpc/server – It’s a peer dependency of @trpc/client .

Create the tRPC Client

Now let’s create the React Query hooks that are specific to our API server with the createTRPCReact() provided by @trpc/react-query binding.

packages/client/src/trpc.ts


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

export const trpc = createTRPCReact<AppRouter>();

Calling our 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 { useState } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { getFetch, httpBatchLink, loggerLink } from "@trpc/client";
import { trpc } from "./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;

Change directory into the root Yarn workspace and run yarn start to start the tRPC client and server.

You should see the message we stored in the Redis database in the browser.

tRPC server and client Redis message

Setup tailwindCss with the React.js tRPC Client

Install tailwindCss and its dependencies

Now change the directory to the packages/client folder and install tailwindCss and its peer dependencies via Yarn.


yarn add -D tailwindcss postcss autoprefixer

Run the init command to generate the tailwind.config.js and postcss.config.js files in the packages/client folder.


npx tailwindcss init -p

Add the Template Paths to the Configuration File

Next, add the paths to the template files in the tailwind.config.js file, and also, remember to 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: [],
};

Add the tailwindCss directives to your CSS

Create a ./packages/client/src/global.css file and add the @tailwind directives. You also need to include your custom font to override the default font that comes with TailwindCSS.

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;
  }
}

Import the CSS file

Now, import the newly-created packages/client/src/global.css file into the packages/client/src/index.tsx file.

packages/client/src/index.tsx


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './global.css';
const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <react.strictmode>
    <app>
  </react.strictmode>
);

Conclusion

Congrats on reaching the end. In this comprehensive article, you’ve learned how to set up tRPC with React.js, Node.js, Redis, and MongoDB.

tRPC Client and Server Project Setup Source Code

You can find the complete source code on GitHub