Create a T3 simple application


WTF is T3 stack?

T3-Stack

Disclaimer: currently the create t3-app isn’t using the new Next.js 13 App Router. We won’t be using it in this article. Check out this effort for more details.

Bootstrapping

Dependencies check

node --version
pnpm --version
docker --version
docker-compose --version

Firing up the create project script

pnpm create t3-app@latest
? What will your project be called? things-t3-demo
? Will you be using TypeScript or JavaScript? TypeScript
? Which packages would you like to enable? nextAuth, prisma, tailwind, trpc
? Initialize a new git repository? Yes
? Would you like us to run 'pnpm install'? Yes
? What import alias would you like configured? @/
Using: pnpmthings-t3-demo scaffolded successfully!

Step into the project folder, run and open it to check how it looks:

cd things-t3-demo
pnpm run dev
# Linux:
xdg-open http://localhost:3000
# MacOS:
open http://localhost:3000
# Windows:
start http://localhost:3000

If all is well, it’s good time to start versioning the code, if you plan to.

git commit -m "initial commit"

Create a PostgreSQL database using Docker Compose

touch docker-compose.yaml
# /docker-compose.yaml
services:
  database:
    container_name: database
    image: postgres:alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=postgres
    volumes:
      - pgdata:/var/lib/postgresql/database

volumes:
  pgdata: {}

Obs.:

  1. Check with docker ps -a if you already have running containers. If you do so, you’ll need extra steps in order to take care of it.
  2. If you have the PostgreSQL service natively installed you can choose to use it instead the container or double check for conflicting port numbers.

Then get the database up and running:

docker-compose up -d

Configure the .env file with the database settings:

# /.env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"

Configure the Prisma ORM file prisma/schema.prisma to match the corresponding provider for PostgreSQL. Take the chance to remove the Example model, it won’t be used. The schema is already populated with the models needed by NextAuth. Also we’re going to create a model named Thing to start the CRUD API.

# /prisma/schema.prisma

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

model Thing {
    id          String   @id @default(cuid())
    name        String   @unique
    description String
    createdAt   DateTime @default(now())
    updatedAt   DateTime @updatedAt
}

To create the migration and synchronize the schema definition with the database, run the following script:

pnpx prisma db push

If you are using VSCode, at this point, you need to restart TypeScript server to refresh the environment with the boilerplate code generated by Prisma.

At any moment you can check the database with your favorite database client. For instance:

psql -h localhost -p 5432 -U postgres -d postgres -W
Password:
psql (15.3)
Type "help" for help.

postgres=# \dt
               List of relations
 Schema |       Name        | Type  |  Owner
--------+-------------------+-------+----------
 public | Account           | table | postgres
 public | Session           | table | postgres
 public | Thing             | table | postgres
 public | User              | table | postgres
 public | VerificationToken | table | postgres
(4 rows)

postgres=# \q

You can play with the database using the Prisma Studio tool. Just run:

pnpx prisma studio

Creating the tRPC API

Navigate your code editor to src/server/api/routers. You can remove the file example.ts, it won’t be used. Create a new file called things.ts at the same level. This file will contain the tRPC endpoints definition for the Thing domain.

The first endpoint we are going to create is the one to retrieve all records. It’s the most simple possible way to start, as we can create some records through the Prisma Studio tool to test it.

The final code will look like this:

// /src/server/api/routers/things.ts
import { createTRPCRouter, publicProcedure } from '@/server/api/trpc'

export const thingsRouter = createTRPCRouter({
  all: publicProcedure.query(({ ctx }) => {
    return ctx.prisma.thing.findMany()
  }),
})

Before being able to test it, we need to modify the root.ts file one level above it. This file is the primary router for our server. We need to manually add the newly created router to it.

The final code will look like this:

// /src/server/api/root.ts
import { createTRPCRouter } from '@/server/api/trpc'
import { thingsRouter } from '@/server/api/routers/things'

export const appRouter = createTRPCRouter({
  things: thingsRouter,
})

export type AppRouter = typeof appRouter

Get the API up and running:

pnpm run dev

Fire a request:

curl --location 'http://localhost:3000/api/trpc/things.all' | jq

If all went well so far, you’ll get something like:

{
  "result": {
    "data": {
      "json": []
    }
  }
}

It’s all fine. Our table is clean. To add some records open the Prisma Studio again and add some things by hand. For instance:

{
	"result": {
		"data": {
			"json": [{
					"id": "cljxeejec0000m21m0veoel00",
					"name": "ball",
					"description": "something nice to kick",
					"createdAt": "2023-07-10T21:51:27.157Z",
					"updatedAt": "2023-07-10T21:51:54.050Z"
				},
				{
					"id": "cljxegzh30001m21mwomusi8y",
					"name": "cat",
					"description": "a lovely pet",
					"createdAt": "2023-07-10T21:53:21.304Z",
					"updatedAt": "2023-07-10T21:53:33.971Z"
				}
			],
			...
		}
	}
}

Alternatively, we could use a testing framework to test the endpoints. At the time of writing this article the most simple framework I have found to test tRPC endpoints was Step CI. If you’re interested, check its documentation to learn how to run it. I’ll leave here an initial configuration capable of testing the humble endpoint we’ve build so far.

# /workflow.yaml
version: '1.1'
name: tRPC
tests:
  example:
    steps:
      - name: Query
        http:
          url: http://localhost:3000/api/trpc
          trpc:
            query:
              things/all: all
          check:
            status: 200

Creating the initial UI: Fetch all records

Before further ado, we are going to create the UI to be able to visualize the same records created before, but through our application. Open the src/pages/index.tsx and remove everything. Make it look like this:

// /src/pages/index.tsx
import { api } from '@/utils/api';

export default function Home() {
  return (
    <div className="mx-auto min-h-screen">
      <h1 className="border-b-4 text-center text-4xl font-bold">

      {/* List everything */}
    </div>
  );
}

At this point, leave the server running and the application open on the browser so you can see the chages you’ll be doing in real time, as you code them. Make sure you left some records on the database table to enjoy. Suspense…

Add the following import statement:

// /src/pages/index.tsx
import { useState } from 'react'

Before the return statement add the following statements:

// /src/pages/index.tsx
// handling state
const [clearForm, setClearForm] = useState(true)

// helper functions
const everything = api.things.all.useQuery()

Finally, insert the following piece of code between the {/* List everything */} and the enclosing </div> tag. Be aware to be looking at the page when you hit save. 8-)

// /src/pages/index.tsx
<div className="mb-4 mt-4 ml-2">
  <h2 className="mb-4 text-2xl font-bold">List everything</h2>
</div>
<button
  className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
  onClick={() => {
    setClearForm(false);
    everything.refetch();
  }}
>
  Fetch
</button>
<button
  className="ml-2 rounded bg-gray-500 px-4 py-2 text-white hover:bg-gray-600"
  onClick={() => setClearForm(true)}
>
  Clear
</button>

<div className="text-center mb-4 mt-4 grid grid-cols-3 gap-4 border-t border-dashed pt-2 font-bold">
  <p>Id</p>
  <p>Name</p>
  <p>Email</p>
</div>

{!clearForm &&
  everything?.data?.map((thing) => (
    <div
      key={thing.id}
      className="my-4 grid grid-cols-3 gap-4 rounded border border-gray-300 bg-white p-4 shadow"
    >
      <p>{thing.id}</p>
      <p>{thing.name}</p>
      <p>{thing.description}</p>
    </div>
  ))}
</div>

Before we dive into the boredom of writing the real Create/Read/Update/Delete API. Let’s play a little bit with securing our API and get our hands dirty with NextAuth.

NextAuth

We will be using GitHub as a third-party authentication provider. By default create t3-app uses Discord. So we are going to do a few changes to make it work accordingly.

Open src/env.mjs and replace all variables mentioning DISCORD with GITHUB, it will look like this:

// /src/env.mjs
...
server: {
  ...
  GITHUB_CLIENT_ID: z.string(),
  GITHUB_CLIENT_SECRET: z.string(),
},
...
  runtimeEnv: {
    ...
    GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
    GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
  }
...

Generate NextAuth secret environment variable

Create and copy it to clipboard:

# Linux
openssl rand -base64 32 | xclip
# MacOS:
pbcopy < openssl rand -base64 32
# Windows:
openssl rand -base64 32 | clip

Add it to the .env file at the project root.

Example:

NEXTAUTH_SECRET="g6rESogW4LAHYICqdbbjol3VUwJm/mLwUXopGVEM5RY="

TO BE CONTINUED…

References