Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
June 26, 2022 09:41 pm GMT

Build a Full Stack App with Next.js, Tailwind, tRPC and Prisma ORM

When we create a TypeScript project that has both a Rest Api and a web app, it becomes challenging to keep type definitions concise in the long run.

If we created a GraphQL Api, the conversation might change because we can use code generation, but we still have to maintain the schema on the backend side.

So basically, in both options, we always have to maintain a schema or some sort of type definition.

Introduction

This is where tRPC comes in, with this toolkit it is possible to create a totally type safe application by only using inference. When we made a small change in the backend, we ended up having those same changes reflected in the frontend.

Prerequisites

Before going further, you need:

  • Node
  • TypeScript
  • Next.js
  • Tailwind
  • NPM

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

Getting Started

Project setup

Let's setup next.js and navigate into the project directory:

npx create-next-app@latest --ts grocery-listcd grocery-list

In tsconfig.json we will add a path alias to make it easier to work with relative paths:

// @/tsconfig.json{  "compilerOptions": {    // ...    "baseUrl": ".",    "paths": {      "@/*": [        "src/*"      ],    }  },  // ...}

Install Tailwind CSS:

npm install @fontsource/poppinsnpm install -D tailwindcss postcss autoprefixernpx tailwindcss init -p

In the file tailwind.config.js add the paths to the pages and components folders:

// @/tailwind.config.jsmodule.exports = {  content: [    "./src/pages/**/*.{js,ts,jsx,tsx}",    "./src/components/**/*.{js,ts,jsx,tsx}",  ],  theme: {    extend: {},  },  plugins: [],}

Now let's add the Tailwind directives to our globals.css:

/* @/src/styles/globals.css */@tailwind base;@tailwind components;@tailwind utilities;* {    font-family: "Poppins";  }

As you may have noticed, all our source code, including the styles, will be inside the /src folder.

Setup Prisma

First of all let's install the necessary dependencies:

npm install prisma

Now let's initialize the 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 GroceryList {  id      Int      @id @default(autoincrement())  title   String  checked Boolean? @default(false)}

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

npx prisma migrate dev --name init

Finally we can install the prisma client:

npm install @prisma/client

With the base configuration of our project complete, we can move on to the next step.

Configure tRPC

First of all, let's make sure that tsconfig.json has strict mode enabled:

// @/tsconfig.json{  "compilerOptions": {    // ...    "strict": true  },  // ...}

Then we can install the following dependencies:

npm install @trpc/client @trpc/server @trpc/react @trpc/next zod react-query

With our dependencies installed we can create the /server folder and we can create our context.

The context is used to pass contextual data to all router resolvers. And in our context we will just pass our prism client instance.

// @/src/server/context.tsimport * as trpc from "@trpc/server";import * as trpcNext from "@trpc/server/adapters/next";import { PrismaClient } from "@prisma/client";export async function createContext(opts?: trpcNext.CreateNextContextOptions) {const prisma = new PrismaClient();return { prisma };}export type Context = trpc.inferAsyncReturnType<typeof createContext>;

With our context created (createContext()) and the data types inferred from it (Context), we can move on to defining our router, but before that it is important to keep in mind that:

  • An endpoint is called a procedure;
  • A procedure can have two types of operations (query and mutation);
  • Queries are responsible for fetching data, while mutations are responsible for making changes to the data (server-side).

With these points in mind we can now define our router:

// @/src/server/router.tsimport * as trpc from "@trpc/server";import { z } from "zod";import { Context } from "./context";export const serverRouter = trpc  .router<Context>()  .query("findAll", {    resolve: async ({ ctx }) => {      return await ctx.prisma.groceryList.findMany();    },  })  .mutation("insertOne", {    input: z.object({      title: z.string(),    }),    resolve: async ({ input, ctx }) => {      return await ctx.prisma.groceryList.create({        data: { title: input.title },      });    },  })  .mutation("updateOne", {    input: z.object({      id: z.number(),      title: z.string(),      checked: z.boolean(),    }),    resolve: async ({ input, ctx }) => {      const { id, ...rest } = input;      return await ctx.prisma.groceryList.update({        where: { id },        data: { ...rest },      });    },  })  .mutation("deleteAll", {    input: z.object({      ids: z.number().array(),    }),    resolve: async ({ input, ctx }) => {      const { ids } = input;      return await ctx.prisma.groceryList.deleteMany({        where: {          id: { in: ids },        },      });    },  });export type ServerRouter = typeof serverRouter;

Based on the previous snippet, you may have noticed the following:

  • The data type of our context was used as a generic in our router so that we have the typed context object (in order to have access to our prisma instance);
  • Our backend has a total of four procedures;
  • We exported our router (serverRouter) and its data type (ServerRouter).

With our router configured, we need to create a API route from Next.js to which we will add our handler api. In our handler api we will pass our router and our context (which is invoked on every request).

// @/src/pages/api/trpc/[trpc].tsimport * as trpcNext from "@trpc/server/adapters/next";import { serverRouter } from "@/server/router";import { createContext } from "@/server/context";export default trpcNext.createNextApiHandler({  router: serverRouter,  createContext,});

Now it's time to configure the _app.tsx file as follows:

// @/src/pages/_app.tsximport "../styles/globals.css";import "@fontsource/poppins";import { withTRPC } from "@trpc/next";import { AppType } from "next/dist/shared/lib/utils";import type { ServerRouter } from "@/server/router";const App: AppType = ({ Component, pageProps }) => {  return <Component {...pageProps} />;};export default withTRPC<ServerRouter>({  config({ ctx }) {    const url = process.env.VERCEL_URL      ? `https://${process.env.VERCEL_URL}/api/trpc`      : "http://localhost:3000/api/trpc";    return { url };  },  ssr: true,})(App);

Then we will be create the tRPC hook, to which we will add the data type of our router as a generic on the createReactQueryHooks() function, so that we can make api calls:

// @/src/utils/trpc.tsimport type { ServerRouter } from "@/server/router";import { createReactQueryHooks } from "@trpc/react";export const trpc = createReactQueryHooks<ServerRouter>();

Create the Frontend

First let's deal with the components of our application, to be simpler I'll put everything in a single file in the /components folder.

Starting with the card, let's create the card's container, header and content:

// @/src/components/index.tsximport React, { memo } from "react";import type { NextPage } from "next";import { GroceryList } from "@prisma/client";interface CardProps {  children: React.ReactNode;}export const Card: NextPage<CardProps> = ({ children }) => {  return (    <div className="h-screen flex flex-col justify-center items-center bg-slate-100">      {children}    </div>  );};export const CardContent: NextPage<CardProps> = ({ children }) => {  return (    <div className="bg-white w-5/6 md:w-4/6 lg:w-3/6 xl:w-2/6 rounded-lg drop-shadow-md">      {children}    </div>  );};interface CardHeaderProps {  title: string;  listLength: number;  clearAllFn?: () => void;}export const CardHeader: NextPage<CardHeaderProps> = ({  title,  listLength,  clearAllFn,}) => {  return (    <div className="flex flex-row items-center justify-between p-3 border-b border-slate-200">      <div className="flex flex-row items-center justify-between">        <h1 className="text-base font-medium tracking-wide text-gray-900 mr-2">          {title}        </h1>        <span className="h-5 w-5 bg-blue-200 text-blue-600 flex items-center justify-center rounded-full text-xs">          {listLength}        </span>      </div>      <button        className="text-sm font-medium text-gray-600 underline"        type="button"        onClick={clearAllFn}      >        Clear all      </button>    </div>  );};// ...

Now that we've created our card, we can create the components of our list:

// @/src/components/index.tsximport React, { memo } from "react";import type { NextPage } from "next";import { GroceryList } from "@prisma/client";// ...export const List: NextPage<CardProps> = ({ children }) => {  return <div className="overflow-y-auto h-72">{children}</div>;};interface ListItemProps {  item: GroceryList;  onUpdate?: (item: GroceryList) => void;}const ListItemComponent: NextPage<ListItemProps> = ({ item, onUpdate }) => {  return (    <div className="h-12 border-b flex items-center justify-start px-3">      <input        type="checkbox"        className="w-4 h-4 border-gray-300 rounded mr-4"        defaultChecked={item.checked as boolean}        onChange={() => onUpdate?.(item)}      />      <h2 className="text-gray-600 tracking-wide text-sm">{item.title}</h2>    </div>  );};export const ListItem = memo(ListItemComponent);// ...

Finally, just create our form to add new elements to the list:

// @/src/components/index.tsximport React, { memo } from "react";import type { NextPage } from "next";import { GroceryList } from "@prisma/client";// ...interface CardFormProps {  value: string;  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;  submit: () => void;}export const CardForm: NextPage<CardFormProps> = ({  value,  onChange,  submit,}) => {  return (    <div className="bg-white w-5/6 md:w-4/6 lg:w-3/6 xl:w-2/6 rounded-lg drop-shadow-md mt-4">      <div className="relative">        <input          className="w-full py-4 pl-3 pr-16 text-sm rounded-lg"          type="text"          placeholder="Grocery item name..."          onChange={onChange}          value={value}        />        <button          className="absolute p-2 text-white -translate-y-1/2 bg-blue-600 rounded-full top-1/2 right-4"          type="button"          onClick={submit}        >          <svg            className="w-4 h-4"            xmlns="http://www.w3.org/2000/svg"            fill="none"            viewBox="0 0 24 24"            stroke="currentColor"          >            <path              strokeLinecap="round"              strokeLinejoin="round"              strokeWidth="2"              d="M12 6v6m0 0v6m0-6h6m-6 0H6"            />          </svg>        </button>      </div>    </div>  );};

And with everything ready, we can start working on our main page. Which can be as follows:

// @/src/pages/index.tsximport type { NextPage } from "next";import Head from "next/head";import { useCallback, useState } from "react";import { trpc } from "@/utils/trpc";import {  Card,  CardContent,  CardForm,  CardHeader,  List,  ListItem,} from "../components/Card";import { GroceryList } from "@prisma/client";const Home: NextPage = () => {  const [itemName, setItemName] = useState<string>("");  const { data: list, refetch } = trpc.useQuery(["findAll"]);  const insertMutation = trpc.useMutation(["insertOne"], {    onSuccess: () => refetch(),  });  const deleteAllMutation = trpc.useMutation(["deleteAll"], {    onSuccess: () => refetch(),  });  const updateOneMutation = trpc.useMutation(["updateOne"], {    onSuccess: () => refetch(),  });  const insertOne = useCallback(() => {    if (itemName === "") return;    insertMutation.mutate({      title: itemName,    });    setItemName("");  }, [itemName, insertMutation]);  const clearAll = useCallback(() => {    if (list?.length) {      deleteAllMutation.mutate({        ids: list.map((item) => item.id),      });    }  }, [list, deleteAllMutation]);  const updateOne = useCallback(    (item: GroceryList) => {      updateOneMutation.mutate({        ...item,        checked: !item.checked,      });    },    [updateOneMutation]  );  return (    <>      <Head>        <title>Grocery List</title>        <meta name="description" content="Visit www.mosano.eu" />        <link rel="icon" href="/favicon.ico" />      </Head>      <main>        <Card>          <CardContent>            <CardHeader              title="Grocery List"              listLength={list?.length ?? 0}              clearAllFn={clearAll}            />            <List>              {list?.map((item) => (                <ListItem key={item.id} item={item} onUpdate={updateOne} />              ))}            </List>          </CardContent>          <CardForm            value={itemName}            onChange={(e) => setItemName(e.target.value)}            submit={insertOne}          />        </Card>      </main>    </>  );};export default Home;

After all these steps in this article, the expected final result is as follows:

image

If you just want to clone the project and create your own version of this app, you can click on this link to access the repository for this article.

I hope you found this article helpful and I'll see you next time.


Original Link: https://dev.to/franciscomendes10866/build-a-full-stack-app-with-nextjs-tailwind-trpc-and-prisma-orm-4ail

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