Build a full stack app with create-t3-app
Tags: webdev, nextjs, tutorial
Published: 2022-08-12 04:27:39

> This blog post is outdated. I no longer have time to keep updating this post.

<br />

Hey there! Today we'll be building an application with the [T3 stack](https://init.tips/). We're going to build a Guestbook inspired by [Lee Robinson's Guestbook](https://leerob.io/guestbook/). Let's get right into it!

<br />

## Getting Started

<br />

Let's set up a starter project with `create-t3-app`!

```bash
npm create t3-app@latest
```

We're going to utilize all parts of the stack.

![create-t3-app](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/WindowsTerminal_oPvbaXQZNk.png)

Let's also set up a Postgres database on [Railway](https://railway.app/). Railway makes it super simple to quickly set up a database.

<br />

Go to [Railway](https://railway.app/) and log in with GitHub if you haven't already. Now click on `New Project`.

![New Project](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_jYkV1HjSF5.png)

Now provision Postgres.

![Provision Postgres](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_pQKKGvQGOM.png)

It's as simple as that. Copy the connection string from the `Connect` tab.

![Copy the connection string](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_YjclbRZxB2.png)

Let's start coding! Open the project in your favourite code editor.

<br />

There are a lot of folders but don't be overwhelmed. Here's a basic overview.

<br />

- `prisma/*` - The `prisma` schema.
- `public/*` - Static assets including fonts and images.
- `src/env/*` - Validation for environment variables.
- `src/pages/*` - All the pages of the website.
- `src/server/*` - The backend, which includes a tRPC server, Prisma client, and auth utility.
- `src/styles/*` - Global CSS files, but we're going to be using Tailwind CSS for most of our styles.
- `src/types/*` - Next Auth type declarations.
- `src/utils/*` - Utility functions.

<br />

Duplicate the `.env.example` file and rename the new copy as `.env`. Open the `.env` file and paste the connection string in `DATABASE_URL`.

<br />

You'll notice we have [Discord](https://discord.com/) OAuth set up using `next-auth`, so we also need a `DISCORD_CLIENT_ID` and `DISCORD_CLIENT_SECRET`. Let's set that up.

<br />

## Setting up authentication

<br />

Go to the [Discord Developers Portal](https://discord.com/developers/) and create a new application.

![New Application on Discord](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_Wn1HquVPUC.png)

Go to `OAuth2/General` and add all of the callback URLs to `Redirects`. For localhost the callback URL is `http://localhost:3000/api/auth/callback/discord`. I also added the production URL ahead of time.

![Add redirects](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_zLSplMIqPu.png)

Copy the client ID and secret and paste both of them into `.env`.

![Copy client ID and secret](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_49rNuyGZ3g.png)

Uncomment `NEXTAUTH_SECRET` and set it as some random string too. Now we have all of our environment variables configured.

<br />

Let's also change the database to `postgresql` and uncomment the `@db.Text` annotations in the `Account` model in `prisma/schema.prisma`. All the models you see in the schema are necessary for Next Auth to work.

![Changes in prisma/schema.prisma](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/WindowsTerminal_dGVMwLI9rH.png)

Let's push this schema to our Railway Postgres database. This command will push our schema to Railway and generate type definitions for the Prisma client.

```bash
npx prisma db push
```

Now run the dev server.

```bash
npm run dev
```

Go to the `src/pages/index.tsx` file and delete all the code, let's just render a heading.

```tsx
// src/pages/index.tsx

const Home = () => {
  return (
    <main>
      <h1>Guestbook</h1>
    </main>
  );
};

export default Home;
```

![Render a heading](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_5idaJbVWcd.png)

I can't look at light themes, so lets apply some global styles in `src/styles/globals.css` to make this app dark theme.

```css
/* src/styles/globals.css */

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

body {
  @apply bg-neutral-900 text-neutral-100;
}
```

![Dark theme](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_9Cyhwtd5ab.png)

Much better.

<br />

If you look at `src/pages/api/auth/[...nextauth].ts`, you can see we have Discord OAuth already set up using Next Auth. Here is where you can add more OAuth providers like Google, Twitter, etc.

<br />

Now let's create a button to let users login with Discord. We can use the [`signIn()` function](https://next-auth.js.org/getting-started/client#signin) from Next Auth.

```tsx
// src/pages/index.tsx

import { signIn } from "next-auth/react";

const Home = () => {
  return (
    <main>
      <h1>Guestbook</h1>
      <button
        onClick={() => {
          signIn("discord").catch(console.log);
        }}
      >
        Login with Discord
       </button>
    </main>
  );
};

export default Home;
```

We can use the [`useSession()` hook](https://next-auth.js.org/getting-started/client#usesession) to get the session for the user. While we're at it, we can also use the [`signOut()` function](https://next-auth.js.org/getting-started/client#signout) to implement log out functionality.

```tsx
// src/pages/index.tsx

import { signIn, signOut, useSession } from "next-auth/react";

const Home = () => {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <main>Loading...</main>;
  }

  return (
    <main>
      <h1>Guestbook</h1>
      <div>
        {session ? (
          <>
            <p>hi {session.user?.name}</p>
            <button
              onClick={() => {
                signOut().catch(console.log);
              }}
            >
              Logout
            </button>
          </>
        ) : (
          <button
            onClick={() => {
              signIn("discord").catch(console.log);
            }}
          >
            Login with Discord
           </button>
        )}
      </div>
    </main>
  );
};

export default Home;
```

![Auth works](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/chrome_bOROpAbB6m.gif)

Great! We now have auth working. Next Auth really makes it stupidly simple.

<br />

## Backend

<br />

Let's work on the backend now. We'll be using [tRPC](https://trpc.io/) for our API layer and [Prisma](https://prisma.io/) for connecting and querying our database.

<br />

We're going to have to modify our prisma schema and add a `Guestbook` model. Each message in the guestbook will have a name and a message. Here's how the model will look like.

```prisma
model Guestbook {
    id        String   @id @default(cuid())
    createdAt DateTime @default(now())
    name      String
    message   String   @db.VarChar(100)
}
```

Let's push this modified schema to our Railway Postgres database.

```bash
npx prisma db push
```

Now let's get to the fun part - it's tRPC time. Go ahead and delete the `example.ts` file in `src/server/api/routers` folder. Then in the same folder, create a new file called `guestbook.ts`.

First, we're going to define a mutation to post messages to our database.

```ts
// src/server/api/routers/guestbook.ts

import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc";

export const guestbookRouter = createTRPCRouter({
  postMessage: publicProcedure
    .input(
      z.object({
        name: z.string(),
        message: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      try {
        await ctx.prisma.guestbook.create({
          data: {
            name: input.name,
            message: input.message,
          },
        });
      } catch (error) {
        console.log(error);
      }
    }),
});
```

Here we have a tRPC mutation that uses [zod](https://zod.dev/) to validate the input and has an async function that runs a single prisma query to create a new row in the `Guestbook` table.

<br />

Working with Prisma is an absolutely wonderful example. The autocomplete and typesafety is amazing.

<br />

We also want this mutation to be protected. Here we can use [tRPC middlewares](https://trpc.io/docs/middlewares).

<br />

If you take a look at the `src/server/auth.ts` file, we're using [`unstable_getServerSession`](https://next-auth.js.org/configuration/nextjs#unstable_getserversession) from Next Auth that gives us access to the session on the server.

```ts
// src/server/auth.ts

import { type GetServerSidePropsContext } from "next";
import { unstable_getServerSession } from "next-auth";
import { authOptions } from "../pages/api/auth/[...nextauth]";

export const getServerAuthSession = async (ctx: {
  req: GetServerSidePropsContext["req"];
  res: GetServerSidePropsContext["res"];
}) => {
  return await unstable_getServerSession(ctx.req, ctx.res, authOptions);
};
```

We're passing that session into our tRPC context.

```ts
// src/server/api/trpc.ts

export const createTRPCContext = async (opts: CreateNextContextOptions) => {
  const { req, res } = opts;
  const session = await getServerAuthSession({ req, res });
  return createInnerTRPCContext({
    session,
  });
};
```

Then, we can use this session to make our mutation protected using a `protectedProcedure`.

```ts
// src/server/api/trpc.ts

const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session || !ctx.session.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({
    ctx: {
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
```

Now, replace `publicProcedure` with `protectedProcedure` to make our mutation protected from unauthenticated users.

```ts
// src/server/api/routers/guestbook.ts

import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";

export const guestbookRouter = createTRPCRouter({
  postMessage: protectedProcedure
    .input(
      z.object({
        name: z.string(),
        message: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      try {
        await ctx.prisma.guestbook.create({
          data: {
            name: input.name,
            message: input.message,
          },
        });
      } catch (error) {
        console.log(error);
      }
    }),
});
```

Next, let's write a query to get all messages in the guestbook. We want all guests to see the messages so we'll use the `publicProcedure` for this.

```ts
// src/server/api/routers/guestbook.ts

import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";

export const guestbookRouter = createTRPCRouter({
  getAll: publicProcedure.query(async ({ ctx }) => {
    try {
      return await ctx.prisma.guestbook.findMany({
        select: {
          name: true,
          message: true,
        },
        orderBy: {
          createdAt: "desc",
        },
      });
    } catch (error) {
      console.log("error", error);
    }
  }),
  //...
```

Here we are getting just the name and message from all the rows from the `Guestbook` model. The rows are sorted in descending order by the `createdAt` field.

<br />
Now merge this router in the main `appRouter`.

```ts
// src/server/api/root.ts

import { createTRPCRouter } from "./trpc";
import { guestbookRouter } from "./routers/guestbook";

export const appRouter = createTRPCRouter({
  guestbook: guestbookRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;
```

We're pretty much done on the backend part. Let's work on the UI next.

<br />

## Frontend

<br />

Let's first center everything.

```tsx
// src/pages/index.tsx

import { signIn, signOut, useSession } from "next-auth/react";

const Home = () => {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <main className="flex flex-col items-center pt-4">Loading...</main>;
  }

  return (
    <main className="flex flex-col items-center">
      <h1 className="text-3xl pt-4">Guestbook</h1>
      <p>
        Tutorial for <code>create-t3-app</code>
      </p>
      <div className="pt-10">
        <div>
          {session ? (
            <>
              <p className="mb-4 text-center">hi {session.user?.name}</p>
              <button
                type="button"
                className="mx-auto block rounded-md bg-neutral-800 py-3 px-6 text-center hover:bg-neutral-700"
                onClick={() => {
                  signOut().catch(console.log);
                }}
              >
                Logout
              </button>
            </>
          ) : (
            <button
              type="button"
              className="mx-auto block rounded-md bg-neutral-800 py-3 px-6 text-center hover:bg-neutral-700"
              onClick={() => {
                signIn("discord").catch(console.log);
              }}
            >
              Login with Discord
            </button>
          )}
        </div>
      </div>
    </main>
  );
};
export default Home;
```

I also made the heading bigger and added some padding between the elements.

<br />

Let's use our tRPC query to get all the messages for the guestbook in the database. But we don't have any data right now. We can use [Prisma Studio](https://prisma.io/studio/) to add some data manually.

```bash
npx prisma studio
```

It will automatically open on `http://localhost:5555`. Go to the `Guestbook` table and add a bunch of records like this.

![Using Prisma Studio](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_YqVtwL8NU0.gif)

Now that we have data, we can use the query and display the data. For this we can use the tRPC `react-query` wrapper. Let's create a component for this in `src/pages/index.tsx`.

```tsx
// src/pages/index.tsx

import { api } from "../utils/api";

const GuestbookEntries = () => {
  const { data: guestbookEntries, isLoading } = api.guestbook.getAll.useQuery();

  if (isLoading) return <div>Fetching messages...</div>;

  return (
    <div className="flex flex-col gap-4">
      {guestbookEntries?.map((entry, index) => {
        return (
          <div key={index}>
            <p>{entry.message}</p>
            <span>- {entry.name}</span>
          </div>
        );
      })}
    </div>
  );
};
```

Here we're using `useQuery()` and mapping over the array it returns.

<br />

Of course here too we have wonderful typesafety and autocomplete.

<br />


Now render this component in the `Home` component.

```tsx
// src/pages/index.tsx

<main className="flex flex-col items-center">
  <h1 className="text-3xl pt-4">Guestbook</h1>
  <p>
    Tutorial for <code>create-t3-app</code>
  </p>
  <div className="pt-10">
    <div>
      {session ? (
        <>
          <p className="mb-4 text-center">hi {session.user?.name}</p>
          <button
            type="button"
            className="mx-auto block rounded-md bg-neutral-800 py-3 px-6 text-center hover:bg-neutral-700"
            onClick={() => {
              signOut().catch(console.log);
            }}
          >
            Logout
          </button>
        </>
      ) : (
        <button
          type="button"
          className="mx-auto block rounded-md bg-neutral-800 py-3 px-6 text-center hover:bg-neutral-700"
          onClick={() => {
            signIn("discord").catch(console.log);
          }}
        >
          Login with Discord
        </button>
      )}
      <div className="pt-10">
        <GuestbookEntries />
      </div>
    </div>
  </div>
</main>
```

Let's now create a form component in `src/pages/index.tsx` and use our tRPC mutation there.

```tsx
// src/pages/index.tsx

const Form = () => {
  const [message, setMessage] = useState("");
  const { data: session, status } = useSession();
  
  const postMessage = api.guestbook.postMessage.useMutation();
  
  if (status !== "authenticated") return null;

  return (
    <form
      className="flex gap-2"
      onSubmit={(event) => {
        event.preventDefault();
        postMessage.mutate({
          name: session.user?.name as string,
          message,
        });
        setMessage("");
      }}
    >
      <input
        type="text"
        className="rounded-md border-2 border-zinc-800 bg-neutral-900 px-4 py-2 focus:outline-none"
        placeholder="Your message..."
        minLength={2}
        maxLength={100}
        value={message}
        onChange={(event) => setMessage(event.target.value)}
      />
      <button
        type="submit"
        className="rounded-md border-2 border-zinc-800 p-2 focus:outline-none"
      >
        Submit
      </button>
    </form>
  );
};
```

We can now render the `Form` in the `Home` component and add some padding.

```tsx
// src/pages/index.tsx

<div>
  {session ? (
    <>
      <p className="mb-4 text-center">hi {session.user?.name}</p>
      <button
        type="button"
        className="mx-auto block rounded-md bg-neutral-800 py-3 px-6 text-center hover:bg-neutral-700"
        onClick={() => {
          signOut().catch(console.log);
        }}
      >
        Logout
      </button>
      <div className="pt-6">
        <Form />
      </div>
    </>
  ) : (
    <button
      type="button"
      className="mx-auto block rounded-md bg-neutral-800 py-3 px-6 text-center hover:bg-neutral-700"
      onClick={() => {
        signIn("discord").catch(console.log);
      }}
    >
      Login with Discord
    </button>
  )}
  <div className="pt-10">
    <GuestbookEntries />
  </div>
</div>
```

Here we have a form and we're using `useMutation()` to post the data to the database. But you'll notice one problem here. When we click on the submit button, it does post the message to the database, but the user doesn't get any immediate feedback. Only on refreshing the page, the user can see the new message.

<br />

For this we can use optimistic UI updates! `react-query` makes this trivial to do. We just need to add some stuff to our `useMutation()` hook inside the `Form` component.

```ts
// src/pages/index.tsx

const utils = api.useContext();
const postMessage = api.guestbook.postMessage.useMutation({
  onMutate: async (newEntry) => {
    await utils.guestbook.getAll.cancel();
    utils.guestbook.getAll.setData(undefined, (prevEntries) => {
      if (prevEntries) {
        return [newEntry, ...prevEntries];
      } else {
        return [newEntry];
      }
    });
  },
  onSettled: async () => {
    await utils.guestbook.getAll.invalidate();
  },
});
```

If you want to learn more about this code example, you can read about optimistic updates with `@tanstack/react-query` [here](https://tanstack.com/query/v4/docs/react/guides/optimistic-updates).

<br />

We're pretty much done with the coding part! That was pretty simple wasn't it. The T3 stack makes it super easy and quick to build full stack web apps. Let's now deploy our guestbook.

<br />

## Deployment

<br />

We're going to use [Vercel](https://vercel.com/?utm_source=t3-oss&utm_campaign=oss) to deploy. Vercel makes it really easy to deploy NextJS apps, they are the people who made NextJS.

<br />

First, push your code to a [GitHub](https://github.com/) repository. Now, go to [Vercel](https://vercel.com/?utm_source=t3-oss&utm_campaign=oss) and sign up with GitHub if you haven't already.

![Vercel](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_fusvFkqV31.png)

Then click on `New Project` and import your newly created repository.

![Import repository](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_HSkZeIQz9z.png)

Now we need to add environment variables, so copy paste all the environment variables to Vercel. After you've done that, click `Deploy`.

![Add environment variables, then click Deploy](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_DPISCJZARJ.png)

Add a custom domain if you have one and you're done! Congratulations!

<br />

All the code can be found [here](https://github.com/nexxeln/guestbook-blog). You can visit the website at [guestbook.nxl.sh](https://guestbook.nxl.sh/).

<br />

## Credits

<br />

- [Ayanava Karmakar](https://github.com/AyanavaKarmakar) for updating the blog using tRPC v10.
- [Julius Marminge](https://github.com/juliusmarminge) and [Michael Lee](https://github.com/vlmlee) for reviewing the updated blog.
- [Lee Robinson](https://leerob.io) for the idea of a guestbook.
- [Anthony](https://twitter.com/ajcwebdev) for giving constructive criticism.
- [JAR](https://github.com/JARofStuff) and [Krish](https://github.com/Krish120003) for proof reading.
- [Hakan Güçlü](https://twitter.com/CreatorX64) for updating the project files in accordance with the latest `create-t3-app` template (7.3.0).