Create a T3 simple application
WTF is 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: pnpm ✔ things-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.:
- 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. - 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="