├── .eslintrc.json ├── .gitignore ├── README.md ├── next-auth.d.ts ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── migrations │ ├── 20220724081705_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── favicon.ico ├── youtube-preview.png └── youtube-thumb.jpg ├── src ├── pages │ ├── _app.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── examples.ts │ │ ├── restricted.ts │ │ └── trpc │ │ │ └── [trpc].ts │ └── index.tsx ├── server │ ├── db │ │ └── client.ts │ ├── env-schema.mjs │ ├── env.mjs │ └── router │ │ ├── auth.ts │ │ ├── context.ts │ │ ├── example.ts │ │ └── index.ts ├── styles │ └── globals.css └── utils │ └── trpc.ts ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | .pnpm-debug.log* 31 | 32 | # local env files 33 | .env 34 | .env*.local 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # T3-Stack inspired resources and tutorial | NextJS, TypeScript, tRPC, Postgres, Prisma, TailwindCSS, nextAuth, Railway, Vercel 2 | 3 | - [About](#about) 4 | - [Video](#video) 5 | - [What's in it?](#whats-in-it) 6 | - [Tech stack](#tech-stack) 7 | - [Products used](#products) 8 | - [Services used](#services) 9 | - [Pricing](#pricing) 10 | - [Instructions](#instructions) 11 | - [Initialise project](#initialise-project) 12 | - [Setup database](#setup-database) 13 | - [Seed data and test app](#seed-data-and-test-app) 14 | - [Deploy](#deploy) 15 | 16 | ## About 17 | 18 | Personal repo for documenting stuff as I go about learning and devving in the initial stack recommended by [t3-oss/create-t3-app](https://github.com/t3-oss/create-t3-app) / [init.tips](https://init.tips), as of July 2022. 19 | 20 | This is not educational material on any of the tech involved, just instructions on how to get them all from nothing to deployed and ready to go as quick and frictionless as possible. 21 | 22 | They all work well together, it's intuitive to pick up, and really efficient to get going on actually creating useful stuff rather than spending hours boilerplating and trying to get disparate things playing nice. As time goes by I'll add other stack resources. 23 | 24 | Initially a compression of a [proposed tutorial](https://github.com/t3-oss/create-t3-app/issues/166#create-t3-app) which has more details for a newcomer. The following is more a quick refresher of just the hit points when you don't need explanations but just to follow the bouncing ball. 25 | 26 | This is not exhaustive nor definitive, just the way I've honed my routine for setting up a full-stack deployed environment within 15 mins and no major headscratching, which includes a front-end, styling, a database, an API, server routing, ~authentication~ (soon), and deployment to public. 27 | 28 | ## What's in it 29 | 30 | In here is everything you need to quickly spin up and deploy a full-stack starter environment with most of the stuff any average app needs for quickly getting to work on your MVP. 31 | 32 | Time required: 33 | 34 | - first time: 45-90mins 35 | - 2nd time: 20-40mins 36 | - subsequent: 10-15mins 37 | 38 | ### Tech stack 39 | 40 | All free and open source. 41 | 42 | | Technology | Description | 43 | | --- | --- | 44 | | `NextJS` | framework | 45 | | `Typescript` | language | 46 | | `tRPC` | API | 47 | | `TailwindCSS` | UI styling and UX fernagling | 48 | | `Postgres` | database | 49 | | `Prisma` | querying and stuff of data | 50 | | ~`nextAuth`~ | authentication / authorisation (coming soon) | 51 | 52 | ### Products 53 | 54 | These are all free and mostly open source. 55 | 56 | Will only include stuff which fits my efficiency paradigm and which I've tested personally, so if you want Yarn or Linux or Vim or all-CLI etc seek elsewhere. But if you're just a regular schmoe dev this is all the stuff you need: 57 | 58 | - Windows 59 | - PowerShell 60 | - NPM 61 | - VSCode 62 | - GitHub Desktop 63 | 64 | ### Services 65 | 66 | These have forever free "hobby" levels, and very cheap entry-level offers once you go into production and need more resources. 67 | 68 | - [GitHub](https://github.com) - code repository 69 | - [Railway](https://railway.app) - database hosting 70 | - [Vercel](https://vercel.com) - deployment server 71 | 72 | ## Pricing 73 | 74 | Free. 75 | 76 | In case you missed it, everything here is free and nearly all open source. 77 | 78 | It's only the online deployed [services](services) which you'll have to pay for if you need a ton of resources (good problem to have), but they all have forever free very generous allocations for messing around. 79 | 80 | Also there's no vendor-lock-in, you can even self-host any of the services, these just represent the easiest for me to use on a whim without costing anything. 81 | 82 | ## Instructions 83 | 84 | There's very little explanation from here on, it's just steps to get the environment up and running and deployed as quick as possible. 85 | 86 | ### Video 87 | 88 | 89 |

90 | Video thumbnail Ash messing with T3-Stack 91 |

92 |
93 | 94 | 95 |

Setup and initial dry run on YouTube

96 |
97 | 98 | - 99 | 100 | I'm hardly a pro YouTuber, this is just my first dry-run following my own tutorial, with minimal explanation. 101 | 102 | Total time: 31mins 103 | Hands-on time: ~15mins 104 | 105 | ### Initialise project 106 | 107 | Powershell: 108 | 109 | ```shell 110 | cd \your-repo-parent-folder 111 | npx create-t3-app@latest 112 | ``` 113 | 114 | ### Name project 115 | 116 | > What will your project be called? `(my-t3-app)` 117 | 118 | ### Illusion of choice 119 | 120 | > Will you be using JavaScript or TypeScript? 121 | 122 | ### Select dev stack 123 | 124 | > Which packages would you like to enable? 125 | > 126 | > `( ) nextAuth` 127 | > `( ) Prisma` 128 | > `( ) TailwindCSS` 129 | > `( ) tRPC` 130 | 131 | I'll take them all thanks. 132 | 133 | ### Git gud 134 | 135 | > Initialize new git repo? 136 | 137 | Yup. 138 | 139 | ### Setup project structure 140 | 141 | > Run NPM install? 142 | 143 | Sure. 144 | 145 | - scaffold: ~30 secs 146 | - install packages: ~60 secs 147 | - Git init: ~1 sec 148 | 149 | ### Publish repo 150 | 151 | GitHub Desktop: 152 | 153 | > Add existing repository 154 | 155 | Select new repo folder that was just set up (already has git stuff in it). 156 | 157 | ### Test project works 158 | 159 | Open in VSCode. 160 | 161 | Terminal: 162 | 163 | ```shell 164 | run npm dev 165 | ``` 166 | 167 | Navigate to `http://localhost:3000`, should see the landing page. 168 | 169 | ## Setup database 170 | 171 | Terminal: 172 | 173 | ```shell 174 | railway login 175 | ``` 176 | 177 | Hit `enter` and it should open a browser and log you into your [Railway account](https://railway.app/dashboard). Can close that window when done. 178 | 179 | Terminal: 180 | 181 | ```shell 182 | railway init 183 | ``` 184 | 185 | > Starting point: 186 | 187 | Select `Empty Project`. (I haven't tried other options yet) 188 | 189 | > Enter project name: 190 | 191 | Just use same name as current project, unless reasons. 192 | 193 | > Import (.env) environment variables? 194 | 195 | No need right now. 196 | 197 | It should auto-open a browser window to the Railway project. 198 | 199 | Terminal: 200 | 201 | ```shell 202 | railway add 203 | ``` 204 | 205 | Select database (postgresql). 206 | 207 | It should populate the Railway project browser window with new empty database. 208 | 209 | ### Populate db credentials 210 | 211 | Railway browser: 212 | 213 | - select the database 214 | - click `Connect` 215 | - copy `Postgres Connection URL` 216 | 217 | VSCode: 218 | 219 | > `/.env` 220 | 221 | ```shell 222 | DATABASE_URL=postgresql://postgres:{password}@containers-blah-69.railway.app:7594/railway 223 | ``` 224 | 225 | > `/prisma/schema.prisma` 226 | 227 | ```js 228 | generator client { 229 | provider = "prisma-client-js" 230 | } 231 | 232 | datasource db { 233 | provider = "postgresql" 234 | url = env("DATABASE_URL") 235 | } 236 | ``` 237 | 238 | ### Create database tables/schema/client 239 | 240 | Terminal: 241 | 242 | ```shell 243 | npx prisma migrate dev --name init 244 | ``` 245 | 246 | Migration takes about 30secs. 247 | 248 | Railway: 249 | 250 | - check tables have been generated 251 | 252 | Terminal: 253 | 254 | ```shell 255 | npx prisma generate 256 | 257 | npx prisma studio 258 | ``` 259 | 260 | Prisma Studio ([http://localhost:5555](http://localhost:5555)) can now be used as a direct interface to the database. 261 | 262 | ## Seed data and test app 263 | 264 | [Prisma Studio](http://localhost:5555): 265 | 266 | - open `Example` table 267 | - click `add record` 268 | - click `save 1 record` 269 | 270 | New row with a GUID entry in `id` field is created. 271 | 272 | Railway: 273 | 274 | - open in browser and check the `example` table has a record in it 275 | 276 | VSCode terminal: 277 | 278 | - `npm run dev` 279 | - nav to `http://localhost:3000/api/examples` 280 | - check the API is running, should just return same record in JSON, eg. 281 | 282 | ```json 283 | [{"id":"cl5yxzjn70013ogxkndjskxr8"}] 284 | ``` 285 | 286 | ## Deploy 287 | 288 | Terminal: 289 | 290 | ```shell 291 | npm add -D vercel 292 | ``` 293 | 294 | - git push changes 295 | - open [Vercel dashboard](https://vercel.com/dashboard) 296 | - click `+ New Project` 297 | 298 | > Import Git Repository 299 | 300 | - select your repo 301 | 302 | > Configure Project 303 | 304 | Add environment variables - these need to be populated in a standard install: 305 | 306 | | Name | Value | 307 | | --- | --- | 308 | | `DATABASE_URL` | `postgresql://postgres:{password}@containers-blah-69.railway.app:7594/railway` | 309 | | `NEXTAUTH_SECRET` | `JustAnythingForNow` | 310 | | `NEXTAUTH_URL` | `http://doesnotmatter:3000` | 311 | | `DISCORD_CLIENT_ID` | `WhateverForNow` | 312 | | `DISCORD_CLIENT_SECRET` | `DealWithItLater` | 313 | 314 | Click `Deploy`. 315 | 316 | Takes 1-2 mins. 317 | 318 | Should now be deployed at `https://your-project-name.vercel.app`. 319 | -------------------------------------------------------------------------------- /next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { DefaultSession } from "next-auth"; 2 | 3 | declare module "next-auth" { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 6 | */ 7 | interface Session { 8 | user?: { 9 | id?: string; 10 | } & DefaultSession["user"]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { env } from "./src/server/env.mjs"; 2 | 3 | /** 4 | * Don't be scared of the generics here. 5 | * All they do is to give us autocompletion when using this. 6 | * 7 | * @template {import('next').NextConfig} T 8 | * @param {T} config - A generic parameter that flows through to the return type 9 | * @constraint {{import('next').NextConfig}} 10 | */ 11 | function defineNextConfig(config) { 12 | return config; 13 | } 14 | 15 | export default defineNextConfig({ 16 | reactStrictMode: true, 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t3-teste08-your-project-name", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate" 11 | }, 12 | "dependencies": { 13 | "@next-auth/prisma-adapter": "^1.0.4", 14 | "@prisma/client": "^4.1.0", 15 | "@trpc/client": "^9.26.2", 16 | "@trpc/next": "^9.26.2", 17 | "@trpc/react": "^9.26.2", 18 | "@trpc/server": "^9.26.2", 19 | "next": "12.2.1", 20 | "next-auth": "^4.10.2", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "react-query": "^3.39.2", 24 | "superjson": "^1.9.1", 25 | "t3-teste08-your-project-name": "file:", 26 | "zod": "^3.17.3" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "18.0.0", 30 | "@types/react": "18.0.14", 31 | "@types/react-dom": "18.0.5", 32 | "autoprefixer": "^10.4.7", 33 | "eslint": "8.18.0", 34 | "eslint-config-next": "12.2.1", 35 | "postcss": "^8.4.14", 36 | "prisma": "^4.1.0", 37 | "tailwindcss": "^3.1.6", 38 | "typescript": "4.7.4", 39 | "vercel": "^27.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/20220724081705_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Example" ( 3 | "id" TEXT NOT NULL, 4 | 5 | CONSTRAINT "Example_pkey" PRIMARY KEY ("id") 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "Account" ( 10 | "id" TEXT NOT NULL, 11 | "userId" TEXT NOT NULL, 12 | "type" TEXT NOT NULL, 13 | "provider" TEXT NOT NULL, 14 | "providerAccountId" TEXT NOT NULL, 15 | "refresh_token" TEXT, 16 | "access_token" TEXT, 17 | "expires_at" INTEGER, 18 | "token_type" TEXT, 19 | "scope" TEXT, 20 | "id_token" TEXT, 21 | "session_state" TEXT, 22 | 23 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 24 | ); 25 | 26 | -- CreateTable 27 | CREATE TABLE "Session" ( 28 | "id" TEXT NOT NULL, 29 | "sessionToken" TEXT NOT NULL, 30 | "userId" TEXT NOT NULL, 31 | "expires" TIMESTAMP(3) NOT NULL, 32 | 33 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 34 | ); 35 | 36 | -- CreateTable 37 | CREATE TABLE "User" ( 38 | "id" TEXT NOT NULL, 39 | "name" TEXT, 40 | "email" TEXT, 41 | "emailVerified" TIMESTAMP(3), 42 | "image" TEXT, 43 | 44 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 45 | ); 46 | 47 | -- CreateTable 48 | CREATE TABLE "VerificationToken" ( 49 | "identifier" TEXT NOT NULL, 50 | "token" TEXT NOT NULL, 51 | "expires" TIMESTAMP(3) NOT NULL 52 | ); 53 | 54 | -- CreateIndex 55 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 56 | 57 | -- CreateIndex 58 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 59 | 60 | -- CreateIndex 61 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 62 | 63 | -- CreateIndex 64 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 65 | 66 | -- CreateIndex 67 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 68 | 69 | -- AddForeignKey 70 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 71 | 72 | -- AddForeignKey 73 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 74 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Example { 14 | id String @id @default(cuid()) 15 | } 16 | 17 | // Necessary for Next auth 18 | model Account { 19 | id String @id @default(cuid()) 20 | userId String 21 | type String 22 | provider String 23 | providerAccountId String 24 | refresh_token String? 25 | access_token String? 26 | expires_at Int? 27 | token_type String? 28 | scope String? 29 | id_token String? 30 | session_state String? 31 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 32 | 33 | @@unique([provider, providerAccountId]) 34 | } 35 | 36 | model Session { 37 | id String @id @default(cuid()) 38 | sessionToken String @unique 39 | userId String 40 | expires DateTime 41 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 42 | } 43 | 44 | model User { 45 | id String @id @default(cuid()) 46 | name String? 47 | email String? @unique 48 | emailVerified DateTime? 49 | image String? 50 | accounts Account[] 51 | sessions Session[] 52 | } 53 | 54 | model VerificationToken { 55 | identifier String 56 | token String @unique 57 | expires DateTime 58 | 59 | @@unique([identifier, token]) 60 | } 61 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshSimmonds/t3-stack-tutorial-and-resources/885a0ae456cdec3a8a5fd7891de4a01239577992/public/favicon.ico -------------------------------------------------------------------------------- /public/youtube-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshSimmonds/t3-stack-tutorial-and-resources/885a0ae456cdec3a8a5fd7891de4a01239577992/public/youtube-preview.png -------------------------------------------------------------------------------- /public/youtube-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshSimmonds/t3-stack-tutorial-and-resources/885a0ae456cdec3a8a5fd7891de4a01239577992/public/youtube-thumb.jpg -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | // src/pages/_app.tsx 2 | import { withTRPC } from "@trpc/next"; 3 | import type { AppRouter } from "../server/router"; 4 | import type { AppType } from "next/dist/shared/lib/utils"; 5 | import superjson from "superjson"; 6 | import { SessionProvider } from "next-auth/react"; 7 | import "../styles/globals.css"; 8 | 9 | const MyApp: AppType = ({ 10 | Component, 11 | pageProps: { session, ...pageProps }, 12 | }) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | const getBaseUrl = () => { 21 | if (typeof window !== "undefined") { 22 | return ""; 23 | } 24 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 25 | 26 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 27 | }; 28 | 29 | export default withTRPC({ 30 | config({ ctx }) { 31 | /** 32 | * If you want to use SSR, you need to use the server's full URL 33 | * @link https://trpc.io/docs/ssr 34 | */ 35 | const url = `${getBaseUrl()}/api/trpc`; 36 | 37 | return { 38 | url, 39 | transformer: superjson, 40 | /** 41 | * @link https://react-query.tanstack.com/reference/QueryClient 42 | */ 43 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, 44 | }; 45 | }, 46 | /** 47 | * @link https://trpc.io/docs/ssr 48 | */ 49 | ssr: false, 50 | })(MyApp); 51 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { type NextAuthOptions } from "next-auth"; 2 | import DiscordProvider from "next-auth/providers/discord"; 3 | import CredentialsProvider from "next-auth/providers/credentials"; 4 | 5 | // Prisma adapter for NextAuth, optional and can be removed 6 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 7 | import { prisma } from "../../../server/db/client"; 8 | import { env } from "../../../server/env.mjs"; 9 | 10 | export const authOptions: NextAuthOptions = { 11 | // Include user.id on session 12 | callbacks: { 13 | session({ session, user }) { 14 | if (session.user) { 15 | session.user.id = user.id; 16 | } 17 | return session; 18 | }, 19 | }, 20 | // Configure one or more authentication providers 21 | adapter: PrismaAdapter(prisma), 22 | providers: [ 23 | DiscordProvider({ 24 | clientId: env.DISCORD_CLIENT_ID, 25 | clientSecret: env.DISCORD_CLIENT_SECRET, 26 | }), 27 | // ...add more providers here 28 | CredentialsProvider({ 29 | name: "Credentials", 30 | credentials: { 31 | name: { 32 | label: "Name", 33 | type: "text", 34 | placeholder: "Enter your name", 35 | }, 36 | }, 37 | async authorize(credentials, _req) { 38 | const user = { id: 1, name: credentials?.name ?? "J Smith" }; 39 | return user; 40 | }, 41 | }), 42 | ], 43 | }; 44 | 45 | export default NextAuth(authOptions); 46 | -------------------------------------------------------------------------------- /src/pages/api/examples.ts: -------------------------------------------------------------------------------- 1 | // src/pages/api/examples.ts 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { prisma } from "../../server/db/client"; 4 | 5 | const examples = async (req: NextApiRequest, res: NextApiResponse) => { 6 | const examples = await prisma.example.findMany(); 7 | res.status(200).json(examples); 8 | }; 9 | 10 | export default examples; 11 | -------------------------------------------------------------------------------- /src/pages/api/restricted.ts: -------------------------------------------------------------------------------- 1 | // Example of a restricted endpoint that only authenticated users can access from https://next-auth.js.org/getting-started/example 2 | 3 | import { NextApiRequest, NextApiResponse } from "next"; 4 | import { unstable_getServerSession as getServerSession } from "next-auth"; 5 | import { authOptions as nextAuthOptions } from "./auth/[...nextauth]"; 6 | 7 | const restricted = async (req: NextApiRequest, res: NextApiResponse) => { 8 | const session = await getServerSession(req, res, nextAuthOptions); 9 | 10 | if (session) { 11 | res.send({ 12 | content: 13 | "This is protected content. You can access this content because you are signed in.", 14 | }); 15 | } else { 16 | res.send({ 17 | error: 18 | "You must be signed in to view the protected content on this page.", 19 | }); 20 | } 21 | }; 22 | 23 | export default restricted; 24 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | // src/pages/api/trpc/[trpc].ts 2 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 3 | import { appRouter } from "../../../server/router"; 4 | import { createContext } from "../../../server/router/context"; 5 | 6 | // export API handler 7 | export default createNextApiHandler({ 8 | router: appRouter, 9 | createContext: createContext, 10 | }); 11 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unknown-property */ 2 | import type { NextPage } from "next"; 3 | import Head from "next/head"; 4 | import { trpc } from "../utils/trpc"; 5 | 6 | type TechnologyCardProps = { 7 | name: string; 8 | description: string; 9 | documentation: string; 10 | }; 11 | 12 | const Home: NextPage = () => { 13 | const hello = trpc.useQuery(["example.hello", { text: "from tRPC" }]); 14 | 15 | return ( 16 | <> 17 | 18 | T3-Stack tutorial | @AshSimmonds | NextJS, TypeScript, tRPC, Postgres, Prisma, Tailwind, nextAuth, Railway, Vercel 19 | 20 | 21 | 22 | 23 |
24 |

25 | Initialise and deploy a T3-Stack app quick and easy 26 |

27 | 28 |

The repo and instructions

29 | 30 | https://github.com/AshSimmonds/t3-stack-tutorial-and-resources 31 | 32 | 33 | 34 | 35 |
36 | 37 |

This stack uses:

38 |
39 | 44 | 49 | 54 | 59 |
60 |
61 | {hello.data ?

{hello.data.greeting}

:

Loading..

} 62 |
63 |
64 | 65 | ); 66 | }; 67 | 68 | const TechnologyCard = ({ 69 | name, 70 | description, 71 | documentation, 72 | }: TechnologyCardProps) => { 73 | return ( 74 |
75 |

{name}

76 |

{description}

77 | 83 | Documentation 84 | 85 |
86 | ); 87 | }; 88 | 89 | export default Home; 90 | -------------------------------------------------------------------------------- /src/server/db/client.ts: -------------------------------------------------------------------------------- 1 | // src/server/db/client.ts 2 | import { PrismaClient } from "@prisma/client"; 3 | import { env } from "../env.mjs"; 4 | 5 | declare global { 6 | var prisma: PrismaClient | undefined; 7 | } 8 | 9 | export const prisma = 10 | global.prisma || 11 | new PrismaClient({ 12 | log: ["query"], 13 | }); 14 | 15 | if (env.NODE_ENV !== "production") { 16 | global.prisma = prisma; 17 | } 18 | -------------------------------------------------------------------------------- /src/server/env-schema.mjs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const envSchema = z.object({ 4 | DATABASE_URL: z.string().url(), 5 | NODE_ENV: z.enum(["development", "test", "production"]), 6 | NEXTAUTH_SECRET: z.string(), 7 | NEXTAUTH_URL: z.string().url(), 8 | DISCORD_CLIENT_ID: z.string(), 9 | DISCORD_CLIENT_SECRET: z.string(), 10 | }); 11 | -------------------------------------------------------------------------------- /src/server/env.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars. 4 | * It has to be a `.mjs`-file to be imported there. 5 | */ 6 | import { envSchema } from "./env-schema.mjs"; 7 | 8 | const _env = envSchema.safeParse(process.env); 9 | 10 | const formatErrors = ( 11 | /** @type {import('zod').ZodFormattedError,string>} */ 12 | errors, 13 | ) => 14 | Object.entries(errors) 15 | .map(([name, value]) => { 16 | if (value && "_errors" in value) 17 | return `${name}: ${value._errors.join(", ")}\n`; 18 | }) 19 | .filter(Boolean); 20 | 21 | if (!_env.success) { 22 | console.error( 23 | "❌ Invalid environment variables:\n", 24 | ...formatErrors(_env.error.format()), 25 | ); 26 | process.exit(1); 27 | } 28 | 29 | export const env = _env.data; 30 | -------------------------------------------------------------------------------- /src/server/router/auth.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from "@trpc/server"; 2 | import { createRouter } from "./context"; 3 | 4 | export const authRouter = createRouter() 5 | .query("getSession", { 6 | resolve({ ctx }) { 7 | return ctx.session; 8 | }, 9 | }) 10 | .middleware(async ({ ctx, next }) => { 11 | // Any queries or mutations after this middleware will 12 | // raise an error unless there is a current session 13 | if (!ctx.session) { 14 | throw new TRPCError({ code: "UNAUTHORIZED" }); 15 | } 16 | return next(); 17 | }) 18 | .query("getSecretMessage", { 19 | async resolve({ ctx }) { 20 | return "You are logged in and can see this secret message!"; 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/server/router/context.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/context.ts 2 | import * as trpc from "@trpc/server"; 3 | import * as trpcNext from "@trpc/server/adapters/next"; 4 | import { unstable_getServerSession as getServerSession } from "next-auth"; 5 | 6 | import { authOptions as nextAuthOptions } from "../../pages/api/auth/[...nextauth]"; 7 | import { prisma } from "../db/client"; 8 | 9 | export const createContext = async ( 10 | opts?: trpcNext.CreateNextContextOptions, 11 | ) => { 12 | const req = opts?.req; 13 | const res = opts?.res; 14 | 15 | const session = 16 | req && res && (await getServerSession(req, res, nextAuthOptions)); 17 | 18 | return { 19 | req, 20 | res, 21 | session, 22 | prisma, 23 | }; 24 | }; 25 | 26 | type Context = trpc.inferAsyncReturnType; 27 | 28 | export const createRouter = () => trpc.router(); 29 | -------------------------------------------------------------------------------- /src/server/router/example.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from "./context"; 2 | import { z } from "zod"; 3 | 4 | export const exampleRouter = createRouter() 5 | .query("hello", { 6 | input: z 7 | .object({ 8 | text: z.string().nullish(), 9 | }) 10 | .nullish(), 11 | resolve({ input }) { 12 | return { 13 | greeting: `Hello ${input?.text ?? "world"}`, 14 | }; 15 | }, 16 | }) 17 | .query("getAll", { 18 | async resolve({ ctx }) { 19 | return await ctx.prisma.example.findMany(); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/server/router/index.ts: -------------------------------------------------------------------------------- 1 | // src/server/router/index.ts 2 | import { createRouter } from "./context"; 3 | import superjson from "superjson"; 4 | 5 | import { exampleRouter } from "./example"; 6 | import { authRouter } from "./auth"; 7 | 8 | export const appRouter = createRouter() 9 | .transformer(superjson) 10 | .merge("example.", exampleRouter) 11 | .merge("auth.", authRouter); 12 | 13 | // export type definition of API 14 | export type AppRouter = typeof appRouter; 15 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/utils/trpc.ts: -------------------------------------------------------------------------------- 1 | // src/utils/trpc.ts 2 | import type { AppRouter } from "../server/router"; 3 | import { createReactQueryHooks } from "@trpc/react"; 4 | import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server"; 5 | 6 | export const trpc = createReactQueryHooks(); 7 | 8 | /** 9 | * This is a helper method to infer the output of a query resolver 10 | * @example type HelloOutput = inferQueryOutput<'hello'> 11 | */ 12 | export type inferQueryOutput< 13 | TRouteKey extends keyof AppRouter["_def"]["queries"], 14 | > = inferProcedureOutput; 15 | 16 | export type inferQueryInput< 17 | TRouteKey extends keyof AppRouter["_def"]["queries"], 18 | > = inferProcedureInput; 19 | 20 | export type inferMutationOutput< 21 | TRouteKey extends keyof AppRouter["_def"]["mutations"], 22 | > = inferProcedureOutput; 23 | 24 | export type inferMutationInput< 25 | TRouteKey extends keyof AppRouter["_def"]["mutations"], 26 | > = inferProcedureInput; 27 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | module.exports = { 4 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "noUncheckedIndexedAccess": true 18 | }, 19 | "include": [ 20 | "next-env.d.ts", 21 | "next-auth.d.ts", 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "**/*.js" 25 | ], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------