Build Full Stack App with React, Fastify, tRPC, Prisma ORM and Turborepo
Tags: javascript, react, tutorial, node
Published: 2022-07-24 14:54:00

In today's article we are going to create a full stack application using a monorepo. Our monorepo will consist of two packages, an api and a web app, which we will create step by step.

# Introduction

In this world of monorepos there are several tools that help us to create and manage our packages/apps. 

And the overwhelming majority of these tools focus on solving just one problem in a very effective way, there are, for example, tools that deal with the versioning of our packages, others generate the build cache, linting and tests, others deal with the from publishing and deploying.

But the purpose of today's article is to use knowledge you already have about creating node apis and web applications in React and simply add some tools to improve our development and delivery experience.

# Prerequisites

Before going further, you need:

- Node
- Yarn
- TypeScript
- React

In addition, you are expected to have basic knowledge of these technologies.

# Getting Started

With these small aspects in mind we can now move on to boostrap our monorepo.

## Yarn workspaces

First of all let's create our project folder:

```sh
mkdir monorepo
cd monorepo
``` 

Then we initialize the repository:

```sh
yarn init -y
```

And in our `package.json` we added the following properties:

```js
{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
}
```

Now we have our workspace configured, and we will have the following benefits:

- Although the dependencies are installed in each package, they will actually be in a single `node_modules/` folder
- Our packages only have binaries or specific versions in the individual `node_modules/` folder
- We are left with a single `yarn.lock` file

Among many other reasons, these are the ones that you will quickly understand in a moment. But now it's time to install a tool that will help us deal with running our packages in parallel as well as optimizing the build of our monorepo.

For this we will install `turborepo` as a development dependency of our workspace:

```sh
yarn add turbo -DW
```

And now we add the turborepo configuration in a file called `turbo.json` with the following pipeline:

```js
{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "dev": {
      "cache": false
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}
```

As you may have noticed in the configuration above, we are not going to take advantage of the cache during the development environment because it makes more sense to use it only at build time (taking into account the example of the article).

With the turborepo configuration, we can now add some scripts to the `package.json` of the root of our workspace:

```js
{
  "name": "@monorepo/root",
  "version": "1.0.0",
  "main": "index.js",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "license": "MIT",
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build"
  },
  "devDependencies": {
    "turbo": "^1.3.1"
  }
}
```

With our workspace created, the turborepo configured, and the scripts needed for today's article, we can proceed to the next step.

## Api Package

First we have to create a `packages/` folder that has been defined in our workspace:

First of all, in the root of our workspace, we have to create a `packages/` folder that has been defined:

```sh
mkdir packages
cd packages
```

Now inside the `packages/` folder we can create each of our packages starting with the creation of our api. First let's create the folder:

```sh
mkdir api
cd api
```

Then let's start the api package repository:

```sh
yarn init -y
```

Now let's create the following `tsconfig.json`:

```js
{
  "compilerOptions": {
    "target": "esnext",
    "module": "CommonJS",
    "allowJs": true,
    "removeComments": true,
    "resolveJsonModule": true,
    "typeRoots": ["./node_modules/@types"],
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "baseUrl": ".",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "Node",
    "skipLibCheck": true,
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
```

And in our `package.json` we have to take into account the name of the package, which by convention is the name of the namespace, like this:

```js
{
  "name": "@monorepo/api",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
}
```

As you may have noticed, the name of the api package is `@monorepo/api` and we still have to take into account the main file of our package, however in today's article we only need to specify where the data types inferred by our router will be, in which case the `main` property of the `package.json` should look like this:

```js
{
  "main": "src/router",
}
```

Now, we can install the necessary dependencies:

```sh
yarn add fastify @fastify/cors @trpc/server zod
yarn add -D @types/node typescript ts-node-dev prisma
```

Then initialize prisma setup:

```sh
npx prisma init
```

And let's add the following schema to our `schema.prisma`:

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

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Note {
  id        Int      @id @default(autoincrement())
  text      String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
```

With the schema defined, you can run our first migration:

```sh
npx prisma migrate dev --name init
```

Finally we can start building the api, starting with defining the tRPC context:

```js
// @/packages/api/src/context/index.ts
import { inferAsyncReturnType } from "@trpc/server";
import { CreateFastifyContextOptions } from "@trpc/server/adapters/fastify";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export const createContext = ({ req, res }: CreateFastifyContextOptions) => {
  return { req, res, prisma };
};

export type Context = inferAsyncReturnType<typeof createContext>;
```

As you can see in the code above, our Prisma instance was created, in our context we can access the Fastify request and response object just as we can access the Prisma instance.

Now we can create the tRPC router of our api, creating only the following procedures:

```js
// @/packages/api/src/router/index.ts
import * as trpc from "@trpc/server";
import { z } from "zod";

import type { Context } from "../context";

export const appRouter = trpc
  .router<Context>()
  .query("getNotes", {
    async resolve({ ctx }) {
      return await ctx.prisma.note.findMany();
    },
  })
  .mutation("createNote", {
    input: z.object({
      text: z.string().min(3).max(245),
    }),
    async resolve({ input, ctx }) {
      return await ctx.prisma.note.create({
        data: {
          text: input.text,
        },
      });
    },
  })
  .mutation("deleteNote", {
    input: z.object({
      id: z.number(),
    }),
    async resolve({ input, ctx }) {
      return await ctx.prisma.note.delete({
        where: {
          id: input.id,
        },
      });
    },
  });

export type AppRouter = typeof appRouter;
```

With the router created, we can proceed to create the main file of our api:

```js
// @/packages/api/src/main.ts
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
import fastify from "fastify";
import cors from "@fastify/cors";

import { createContext } from "./context";
import { appRouter } from "./router";

const app = fastify({ maxParamLength: 5000 });

app.register(cors, { origin: "*" });

app.register(fastifyTRPCPlugin, {
  prefix: "/trpc",
  trpcOptions: { router: appRouter, createContext },
});

(async () => {
  try {
    await app.listen({ port: 5000 });
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
})();
```

Again in the `package.json` of the api, we added the following scripts:

```js
{
  "scripts": {
    "dev": "tsnd --respawn --transpile-only src/main.ts",
    "build": "tsc",
    "start": "node dist/main.js"
  },
}
```

With our API configured, we can now move on to the creation and configuration of our web app.

## Web App Package

Unlike what we did with the api, we are not going to do the configuration from absolute zero. Now, again inside the `packages/` folder let's run the following command to boostrap a react application using vite:

```sh
yarn create vite web --template react-ts
cd web
```

So, now inside the `packages/` folder we have two folders (`api/` and `web/`) that correspond to our api and our web app respectively.

Inside the folder of our `web/` package, we will install the following dependencies:

```sh
yarn add @trpc/server zod @trpc/client @trpc/server @trpc/react react-query @nextui-org/react formik
```

Next we will create our tRPC hook and we will import the router types from our `api/` package:

```js
// @/packages/web/src/hooks/trpc.ts
import { createReactQueryHooks } from "@trpc/react";
import type { AppRouter } from "@monorepo/api";

export const trpc = createReactQueryHooks<AppRouter>();
```

Now in the `main.tsx` file we will add the UI library provider that we are going to use:

```js
// @/packages/web/src/main.tsx
import ReactDOM from "react-dom/client";
import { NextUIProvider } from '@nextui-org/react';

import App from "./App";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <NextUIProvider>
    <App />
  </NextUIProvider>
);
```

Now in the `App.tsx` file we can proceed to configure the tRPC provider and React Query:

```js
// @/packages/web/src/App.tsx
import { useMemo } from "react";
import { QueryClient, QueryClientProvider } from "react-query";

import { trpc } from "./hooks/trpc";
import AppBody from "./components/AppBody";

const App = () => {
  const queryClient = useMemo(() => new QueryClient(), []);
  const trpcClient = useMemo(
    () =>
      trpc.createClient({
        url: "http://localhost:5000/trpc",
      }),
    []
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <AppBody />
      </QueryClientProvider>
    </trpc.Provider>
  );
};

export default App;
```

As you may have noticed, the `<AppBody />` component hasn't been created yet and that's exactly what we're going to create now:

```js
// @/packages/web/src/components/AppBody.tsx
import {
  Card,
  Text,
  Container,
  Textarea,
  Button,
  Grid,
} from "@nextui-org/react";
import { useCallback } from "react";
import { useFormik } from "formik";

import { trpc } from "../hooks/trpc";
interface IFormFields {
  content: string;
}

const AppBody = () => {
  const utils = trpc.useContext();
  const getNotes = trpc.useQuery(["getNotes"]);
  const createNote = trpc.useMutation(["createNote"]);
  const deleteNote = trpc.useMutation(["deleteNote"]);

  const formik = useFormik<IFormFields>({
    initialValues: {
      content: "",
    },
    onSubmit: async (values) => {
      await createNote.mutateAsync(
        {
          text: values.content,
        },
        {
          onSuccess: () => {
            utils.invalidateQueries(["getNotes"]);
            formik.resetForm();
          },
        }
      );
    },
  });

  const handleNoteRemoval = useCallback(async (id: number) => {
    await deleteNote.mutateAsync(
      {
        id,
      },
      {
        onSuccess: () => {
          utils.invalidateQueries(["getNotes"]);
        },
      }
    );
  }, []);

  return (
    <Container>
      <form
        onSubmit={formik.handleSubmit}
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "center",
          alignItems: "center",
          marginBottom: 50,
          marginTop: 50,
        }}
      >
        <Textarea
          underlined
          color="primary"
          labelPlaceholder="Type something..."
          name="content"
          value={formik.values.content}
          onChange={formik.handleChange}
          css={{ width: 350 }}
        />
        <Button
          shadow
          color="primary"
          auto
          css={{ marginLeft: 25 }}
          size="lg"
          type="submit"
        >
          Create
        </Button>
      </form>
      <Grid.Container gap={2}>
        {getNotes.data?.map((note) => (
          <Grid xs={4} key={note.id} onClick={() => handleNoteRemoval(note.id)}>
            <Card isHoverable variant="bordered" css={{ cursor: "pointer" }}>
              <Card.Body>
                <Text
                  h4
                  css={{
                    textGradient: "45deg, $blue600 -20%, $pink600 50%",
                  }}
                  weight="bold"
                >
                  {note.text}
                </Text>
              </Card.Body>
            </Card>
          </Grid>
        ))}
      </Grid.Container>
    </Container>
  );
};

export default AppBody;
```

In the component above, we use the formik library to validate and manage the form of our component, which in this case has only one input. As soon as a note is created or deleted, we invalidate the `getNotes` query so that the UI is always up to date.

## How to run

If you want to initialize the development environment, in order to work on packages, run the following command in the project root:

```sh
yarn dev
```

If you want to build packages, run the following command in the project root:

```sh
yarn build
```

# Conclusion

As always, I hope you enjoyed this article and that it was useful to you. If you have seen any errors in the article, please let me know in the comments so that I can correct them.

Before I finish, I will share with you [this link](https://github.com/FranciscoMendes10866/react-fastify-trpc-turborepo) to the github repository with the project code for this article.

![bye bye gif](https://i.giphy.com/media/w89ak63KNl0nJl80ig/giphy.webp)