Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 24, 2022 02:54 pm GMT

Build Full Stack App with React, Fastify, tRPC, Prisma ORM and Turborepo

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:

mkdir monorepocd monorepo

Then we initialize the repository:

yarn init -y

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

{  "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:

yarn add turborepo -DW

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

{  "$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:

{  "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:

mkdir packagescd 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:

mkdir apicd api

Then let's start the api package repository:

yarn init -y

Now let's create the following tsconfig.json:

{  "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:

{  "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:

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

Now, we can install the necessary dependencies:

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

Then initialize prisma setup:

npx prisma init

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

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:

npx prisma migrate dev --name init

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

// @/packages/api/src/context/index.tsimport { 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:

// @/packages/api/src/router/index.tsimport * 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:

// @/packages/api/src/main.tsimport { 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:

{  "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:

yarn create vite web --template react-tscd 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:

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:

// @/packages/web/src/hooks/trpc.tsimport { 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:

// @/packages/web/src/main.tsximport 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:

// @/packages/web/src/App.tsximport { 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:

// @/packages/web/src/components/AppBody.tsximport {  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:

yarn dev

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

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 to the github repository with the project code for this article.

bye bye gif


Original Link: https://dev.to/franciscomendes10866/build-full-stack-app-with-react-fastify-trpc-prisma-orm-and-turborepo-k24

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To