├── .env
├── .eslintrc
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .stackblitzrc
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── playwright.config.ts
├── playwright
└── smoke.test.ts
├── prisma
├── migrations
│ ├── 20220706070154_init
│ │ └── migration.sql
│ └── migration_lock.toml
├── schema.prisma
└── seed.ts
├── public
├── favicon.ico
└── vercel.svg
├── render.yaml
├── sandbox.config.json
├── src
├── components
│ └── DefaultLayout.tsx
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── trpc
│ │ │ └── [trpc].ts
│ ├── index.tsx
│ └── post
│ │ └── [id].tsx
├── server
│ ├── context.ts
│ ├── env.js
│ ├── prisma.ts
│ ├── routers
│ │ ├── _app.ts
│ │ ├── health.ts
│ │ ├── post.test.ts
│ │ └── post.ts
│ └── trpc.ts
└── utils
│ ├── publicRuntimeConfig.ts
│ └── trpc.ts
├── tsconfig.json
└── vitest.config.ts
/.env:
--------------------------------------------------------------------------------
1 | # Make sure to override these in deployment
2 | # DATABASE_URL=postgresql://postgres:@localhost:5432/next-prisma-starter-new
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser", // Specifies the ESLint parser
3 | "extends": [
4 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
5 | "plugin:react/recommended",
6 | "plugin:react-hooks/recommended",
7 | "plugin:prettier/recommended"
8 | ],
9 | "parserOptions": {
10 | "project": "tsconfig.json",
11 | "ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
12 | "sourceType": "module" // Allows for the use of imports
13 | },
14 | "rules": {
15 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
16 | "@typescript-eslint/explicit-function-return-type": "off",
17 | "@typescript-eslint/explicit-module-boundary-types": "off",
18 | "react/react-in-jsx-scope": "off",
19 | "react/prop-types": "off",
20 | "@typescript-eslint/no-explicit-any": "off"
21 | },
22 | // "overrides": [
23 | // {
24 | // "files": [],
25 | // "rules": {
26 | // "@typescript-eslint/no-unused-vars": "off"
27 | // }
28 | // }
29 | // ],
30 | "settings": {
31 | "react": {
32 | "version": "detect"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: KATT
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 2
8 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: E2E-testing
2 | on: [push]
3 | jobs:
4 | e2e:
5 | env:
6 | NODE_ENV: test
7 | NEXTAUTH_SECRET: supersecret
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | matrix:
11 | node: ['16.x']
12 | os: [ubuntu-latest]
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v3
16 | with:
17 | fetch-depth: 0
18 |
19 | - uses: pnpm/action-setup@v2.2.2
20 | with:
21 | version: 7.12.0
22 |
23 | - name: Use Node ${{ matrix.node }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node }}
27 | # cache: 'pnpm' # You can active this cache when your repo has a lockfile
28 |
29 | - name: Install deps (with cache)
30 | run: pnpm install
31 |
32 | - name: Next.js cache
33 | uses: actions/cache@v3
34 | with:
35 | path: ${{ github.workspace }}/.next/cache
36 | key: ${{ runner.os }}-${{ runner.node }}-${{ hashFiles('**/pnpm-lock.yaml') }}-nextjs
37 |
38 | - name: Build and test
39 | run: pnpm build && pnpm test-start && pnpm test-dev
40 |
41 | - name: Upload test results
42 | if: ${{ always() }}
43 | uses: actions/upload-artifact@v2
44 | with:
45 | name: test results
46 | path: |
47 | playwright/test-results
48 |
--------------------------------------------------------------------------------
/.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 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | *.db
37 | *.db-journal
38 |
39 |
40 | # testing
41 | playwright/test-results
42 |
--------------------------------------------------------------------------------
/.stackblitzrc:
--------------------------------------------------------------------------------
1 | {
2 | "installDependencies": true,
3 | "startCommand": "yarn dx",
4 | "env": {
5 | "NODE_ENV": "development"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "esbenp.prettier-vscode",
4 | "dbaeumer.vscode-eslint",
5 | "prisma.prisma"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Prisma + tRPC
2 |
3 |
4 | > V10 Preview of tRPC!
5 |
6 | - 📚 See docs here: https://alpha.trpc.io/docs
7 | - 🙏 Feel free to open issues in this repo to give feedback!
8 | - ❓ Search the project for `QUESTION` for open API discussions, but don't feel limited to *only* give feedback on those!
9 | - ⚡ ~~Open in CodeSandbox: [https://codesandbox.io/s/github/trpc/examples-v10-next-prisma-starter-sqlite](https://codesandbox.io/s/github/trpc/examples-v10-next-prisma-starter-sqlite?file=/src/pages/post/%5Bid%5D.tsx)~~ Inference on CodeSandbox currently doesn't work, so you'll have to open the project locally
10 |
11 |
44 |
45 |
46 | ### Requirements
47 |
48 | - Node >= 14
49 |
50 |
51 | ## Development
52 |
53 | ### Clone & start project
54 |
55 | ```bash
56 | yarn create next-app --example https://github.com/trpc/examples-v10-next-prisma-starter-sqlite trpc-prisma-starter
57 | cd trpc-prisma-starter
58 | yarn
59 | yarn dev
60 | ```
61 |
62 | ### Commands
63 |
64 | ```bash
65 | yarn build # runs `prisma generate` + `prisma migrate` + `next build`
66 | yarn db-reset # resets local db
67 | yarn dev # does db changes + starts next.js
68 | yarn test-dev # runs e2e tests on dev
69 | yarn test-start # runs e2e tests on `next start` - build required before
70 | yarn test:unit # runs normal jest unit tests
71 | yarn test:e2e # runs e2e tests
72 | ```
73 |
74 | ## Deployment
75 |
76 | ### Using [Render](https://render.com/)
77 |
78 | The project contains a [`render.yaml`](./render.yaml) [*"Blueprint"*](https://render.com/docs/blueprint-spec) which makes the project easily deployable on [Render](https://render.com/).
79 |
80 | Go to [dashboard.render.com/blueprints](https://dashboard.render.com/blueprints) and connect to this Blueprint and see how the app and database automatically gets deployed.
81 |
82 | ## Files of note
83 |
84 |
106 |
107 | ---
108 |
109 | Created by [@alexdotjs](https://twitter.com/alexdotjs).
110 |
--------------------------------------------------------------------------------
/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.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | /* eslint-disable @typescript-eslint/no-var-requires */
3 | const { env } = require('./src/server/env');
4 |
5 | /**
6 | * Don't be scared of the generics here.
7 | * All they do is to give us autocompletion when using this.
8 | *
9 | * @template {import('next').NextConfig} T
10 | * @param {T} config - A generic parameter that flows through to the return type
11 | * @constraint {{import('next').NextConfig}}
12 | */
13 | function getConfig(config) {
14 | return config;
15 | }
16 |
17 | /**
18 | * @link https://nextjs.org/docs/api-reference/next.config.js/introduction
19 | */
20 | module.exports = getConfig({
21 | /**
22 | * Dynamic configuration available for the browser and server.
23 | * Note: requires `ssr: true` or a `getInitialProps` in `_app.tsx`
24 | * @link https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration
25 | */
26 | publicRuntimeConfig: {
27 | NODE_ENV: env.NODE_ENV,
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@examples/next-starter-sqlite",
3 | "version": "10.0.0-proxy-beta.17",
4 | "private": true,
5 | "scripts": {
6 | "build:1-migrate": "prisma migrate deploy",
7 | "build:2-build": "next build",
8 | "build": "run-s build:*",
9 | "db-seed": "prisma db seed",
10 | "db-migrate-dev": "prisma migrate dev",
11 | "db-reset": "prisma migrate reset",
12 | "dev:next": "next dev",
13 | "dev": "run-s db-migrate-dev db-seed dev:next",
14 | "start": "next start",
15 | "lint": "eslint src",
16 | "lint-fix": "pnpm lint --fix",
17 | "test-dev": "start-server-and-test dev 3000 test",
18 | "test-start": "start-server-and-test start 3000 test",
19 | "test": "run-s test:*",
20 | "test:unit": "vitest run",
21 | "test:e2e": "playwright test",
22 | "postinstall": "prisma generate"
23 | },
24 | "prisma": {
25 | "seed": "tsx prisma/seed.ts"
26 | },
27 | "prettier": {
28 | "printWidth": 80,
29 | "trailingComma": "all",
30 | "singleQuote": true
31 | },
32 | "dependencies": {
33 | "@prisma/client": "^4.3.1",
34 | "@tanstack/react-query": "^4.3.8",
35 | "@trpc/client": "^10.0.0-proxy-beta.17",
36 | "@trpc/next": "^10.0.0-proxy-beta.17",
37 | "@trpc/react": "^10.0.0-proxy-beta.17",
38 | "@trpc/server": "^10.0.0-proxy-beta.17",
39 | "clsx": "^1.1.1",
40 | "next": "^12.3.1",
41 | "react": "^18.2.0",
42 | "react-dom": "^18.2.0",
43 | "superjson": "^1.7.4",
44 | "zod": "^3.0.0"
45 | },
46 | "devDependencies": {
47 | "@playwright/test": "^1.26.1",
48 | "@tanstack/react-query-devtools": "^4.3.8",
49 | "@types/node": "^18.7.20",
50 | "@types/react": "^18.0.9",
51 | "@typescript-eslint/eslint-plugin": "^5.37.0",
52 | "@typescript-eslint/parser": "^5.37.0",
53 | "eslint": "^7.32.0",
54 | "eslint-config-next": "^12.3.1",
55 | "eslint-config-prettier": "^8.5.0",
56 | "eslint-plugin-prettier": "^4.0.0",
57 | "eslint-plugin-react": "^7.25.1",
58 | "eslint-plugin-react-hooks": "^4.2.0",
59 | "npm-run-all": "^4.1.5",
60 | "prettier": "^2.7.1",
61 | "prisma": "^4.3.1",
62 | "start-server-and-test": "^1.12.0",
63 | "tsx": "^3.9.0",
64 | "typescript": "^4.8.3",
65 | "vite": "^3.1.3",
66 | "vitest": "^0.23.4"
67 | },
68 | "publishConfig": {
69 | "access": "restricted"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { PlaywrightTestConfig, devices } from '@playwright/test';
2 |
3 | const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';
4 | console.log(`ℹ️ Using base URL "${baseUrl}"`);
5 |
6 | const opts = {
7 | // launch headless on CI, in browser locally
8 | headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
9 | // collectCoverage: !!process.env.PLAYWRIGHT_HEADLESS
10 | };
11 | const config: PlaywrightTestConfig = {
12 | testDir: './playwright',
13 | outputDir: './playwright/test-results',
14 | // 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
15 | // default 'list' when running locally
16 | reporter: process.env.CI ? 'github' : 'list',
17 | use: {
18 | ...devices['Desktop Chrome'],
19 | baseURL: baseUrl,
20 | headless: opts.headless,
21 | video: 'on',
22 | },
23 | };
24 |
25 | export default config;
26 |
--------------------------------------------------------------------------------
/playwright/smoke.test.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test.setTimeout(35e3);
4 |
5 | test('go to /', async ({ page }) => {
6 | await page.goto('/');
7 |
8 | await page.waitForSelector(`text=Starter`);
9 | });
10 |
11 | test('test 404', async ({ page }) => {
12 | const res = await page.goto('/post/not-found');
13 | expect(res?.status()).toBe(404);
14 | });
15 |
16 | test('add a post', async ({ page, browser }) => {
17 | const nonce = `${Math.random()}`;
18 |
19 | await page.goto('/');
20 | await page.fill(`[name=title]`, nonce);
21 | await page.fill(`[name=text]`, nonce);
22 | await page.click(`form [type=submit]`);
23 | await page.waitForLoadState('networkidle');
24 | await page.reload();
25 |
26 | expect(await page.content()).toContain(nonce);
27 |
28 | const ssrContext = await browser.newContext({
29 | javaScriptEnabled: false,
30 | });
31 | const ssrPage = await ssrContext.newPage();
32 | await ssrPage.goto('/');
33 |
34 | expect(await ssrPage.content()).toContain(nonce);
35 | });
36 |
37 | test('server-side rendering test', async ({ page, browser }) => {
38 | // add a post
39 | const nonce = `${Math.random()}`;
40 |
41 | await page.goto('/');
42 | await page.fill(`[name=title]`, nonce);
43 | await page.fill(`[name=text]`, nonce);
44 | await page.click(`form [type=submit]`);
45 | await page.waitForLoadState('networkidle');
46 |
47 | // load the page without js
48 | const ssrContext = await browser.newContext({
49 | javaScriptEnabled: false,
50 | });
51 | const ssrPage = await ssrContext.newPage();
52 | await ssrPage.goto('/');
53 | expect(await ssrPage.content()).toContain(nonce);
54 | });
55 |
--------------------------------------------------------------------------------
/prisma/migrations/20220706070154_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Post" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "title" TEXT NOT NULL,
5 | "text" TEXT NOT NULL,
6 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
7 | "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
8 | );
9 |
--------------------------------------------------------------------------------
/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 = "sqlite"
--------------------------------------------------------------------------------
/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 | datasource db {
5 | provider = "sqlite"
6 | url = "file:./dev.db"
7 | }
8 |
9 | generator client {
10 | provider = "prisma-client-js"
11 | }
12 |
13 | model Post {
14 | id String @id @default(uuid())
15 | title String
16 | text String
17 |
18 | // To return `Date`s intact through the API we need to add data transformers
19 | // https://trpc.io/docs/data-transformers
20 | createdAt DateTime @default(now())
21 | updatedAt DateTime @default(now()) @updatedAt
22 | }
23 |
--------------------------------------------------------------------------------
/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Adds seed data to your db
3 | *
4 | * @link https://www.prisma.io/docs/guides/database/seed-database
5 | */
6 | import { PrismaClient } from '@prisma/client';
7 |
8 | const prisma = new PrismaClient();
9 |
10 | async function main() {
11 | const firstPostId = '5c03994c-fc16-47e0-bd02-d218a370a078';
12 | await prisma.post.upsert({
13 | where: {
14 | id: firstPostId,
15 | },
16 | create: {
17 | id: firstPostId,
18 | title: 'First Post',
19 | text: 'This is an example post generated from `prisma/seed.ts`',
20 | },
21 | update: {},
22 | });
23 | }
24 |
25 | main()
26 | .catch((e) => {
27 | console.error(e);
28 | process.exit(1);
29 | })
30 | .finally(async () => {
31 | await prisma.$disconnect();
32 | });
33 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trpc/examples-v10-next-prisma-starter-sqlite/62b1f1f98f634f77aa8483d093bea00dd298bc27/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/render.yaml:
--------------------------------------------------------------------------------
1 | #### Render Blueprint specification: https://dashboard.render.com/blueprints ####
2 | ## 👇 Preview environments: https://render.com/docs/preview-environments ###
3 | # previewsEnabled: true
4 | ## 👇 Automatically nuke the environment after X days of inactivity to reduce billing:
5 | # previewsExpireAfterDays: 2
6 | services:
7 | - type: web
8 | name: trpc-starter-app
9 | env: node
10 | plan: free
11 | ## 👇 Specify the plan for the PR deployment:
12 | # previewPlan: starter
13 | ## 👇 Preview Environment Initialization script:
14 | # initialDeployHook: yarn db-seed
15 | buildCommand: pnpm install && pnpm build
16 | startCommand: pnpm start
17 | healthCheckPath: /api/trpc/healthz
18 | envVars:
19 | - key: DATABASE_URL
20 | fromDatabase:
21 | name: trpc-starter-db
22 | property: connectionString
23 |
24 | databases:
25 | - name: trpc-starter-db
26 | plan: free
27 |
--------------------------------------------------------------------------------
/sandbox.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "next",
3 | "container": {
4 | "node": "16",
5 | "startScript": "dev"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/DefaultLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
2 | import Head from 'next/head';
3 | import { ReactNode } from 'react';
4 |
5 | type DefaultLayoutProps = { children: ReactNode };
6 |
7 | export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
8 | return (
9 | <>
10 |
11 | Prisma Starter
12 |
13 |
14 |
15 | {children}
16 |
17 | {process.env.NODE_ENV !== 'production' && (
18 |
19 | )}
20 | >
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from 'next';
2 | import type { AppType, AppProps } from 'next/app';
3 | import type { ReactElement, ReactNode } from 'react';
4 | import { DefaultLayout } from '~/components/DefaultLayout';
5 | import { trpc } from '~/utils/trpc';
6 |
7 | export type NextPageWithLayout<
8 | TProps = Record,
9 | TInitialProps = TProps,
10 | > = NextPage & {
11 | getLayout?: (page: ReactElement) => ReactNode;
12 | };
13 |
14 | type AppPropsWithLayout = AppProps & {
15 | Component: NextPageWithLayout;
16 | };
17 |
18 | const MyApp = (({ Component, pageProps }: AppPropsWithLayout) => {
19 | const getLayout =
20 | Component.getLayout ?? ((page) => {page});
21 |
22 | return getLayout();
23 | }) as AppType;
24 |
25 | export default trpc.withTRPC(MyApp);
26 |
--------------------------------------------------------------------------------
/src/pages/api/trpc/[trpc].ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains tRPC's HTTP response handler
3 | */
4 | import * as trpcNext from '@trpc/server/adapters/next';
5 | import { createContext } from '~/server/context';
6 | import { appRouter } from '~/server/routers/_app';
7 |
8 | export default trpcNext.createNextApiHandler({
9 | router: appRouter,
10 | /**
11 | * @link https://trpc.io/docs/context
12 | */
13 | createContext,
14 | /**
15 | * @link https://trpc.io/docs/error-handling
16 | */
17 | onError({ error }) {
18 | if (error.code === 'INTERNAL_SERVER_ERROR') {
19 | // send to bug reporting
20 | console.error('Something went wrong', error);
21 | }
22 | },
23 | /**
24 | * Enable query batching
25 | */
26 | batching: {
27 | enabled: true,
28 | },
29 | /**
30 | * @link https://trpc.io/docs/caching#api-response-caching
31 | */
32 | // responseMeta() {
33 | // // ...
34 | // },
35 | });
36 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { trpc } from '../utils/trpc';
2 | import { NextPageWithLayout } from './_app';
3 | import Link from 'next/link';
4 | import React from 'react';
5 |
6 | const IndexPage: NextPageWithLayout = () => {
7 | const utils = trpc.useContext();
8 | const postsQuery = trpc.post.list.useQuery();
9 |
10 | const addPost = trpc.post.add.useMutation({
11 | async onSuccess() {
12 | // refetches posts after a post is added
13 | await utils.post.list.invalidate();
14 | },
15 | });
16 |
17 | // prefetch all posts for instant navigation
18 | // React.useEffect(() => {
19 | // for (const { id } of postsQuery.data ?? []) {
20 | // utils.post.byId.prefetch({ id });
21 | // }
22 | // }, [postsQuery.data, utils]);
23 |
24 | return (
25 | <>
26 | Welcome to your tRPC starter!
27 |
28 | Check the docs whenever you get
29 | stuck, or ping @alexdotjs on
30 | Twitter.
31 |
32 |
33 |
34 | Posts
35 | {postsQuery.status === 'loading' && '(loading)'}
36 |
37 | {postsQuery.data?.map((item) => (
38 |
39 | {item.title}
40 |
41 | View more
42 |
43 |
44 | ))}
45 |
46 |
47 |
48 |
90 | >
91 | );
92 | };
93 |
94 | export default IndexPage;
95 |
96 | /**
97 | * If you want to statically render this page
98 | * - Export `appRouter` & `createContext` from [trpc].ts
99 | * - Make the `opts` object optional on `createContext()`
100 | *
101 | * @link https://trpc.io/docs/ssg
102 | */
103 | // export const getStaticProps = async (
104 | // context: GetStaticPropsContext<{ filter: string }>,
105 | // ) => {
106 | // const ssg = createSSGHelpers({
107 | // router: appRouter,
108 | // ctx: await createContext(),
109 | // });
110 | //
111 | // await ssg.fetchQuery('post.all');
112 | //
113 | // return {
114 | // props: {
115 | // trpcState: ssg.dehydrate(),
116 | // filter: context.params?.filter ?? 'all',
117 | // },
118 | // revalidate: 1,
119 | // };
120 | // };
121 |
--------------------------------------------------------------------------------
/src/pages/post/[id].tsx:
--------------------------------------------------------------------------------
1 | import NextError from 'next/error';
2 | import { useRouter } from 'next/router';
3 | import { NextPageWithLayout } from '~/pages/_app';
4 | import { trpc } from '~/utils/trpc';
5 |
6 | const PostViewPage: NextPageWithLayout = () => {
7 | const id = useRouter().query.id as string;
8 | const postQuery = trpc.post.byId.useQuery({ id });
9 |
10 | if (postQuery.error) {
11 | return (
12 |
16 | );
17 | }
18 |
19 | if (postQuery.status !== 'success') {
20 | return <>Loading...>;
21 | }
22 | const { data } = postQuery;
23 | return (
24 | <>
25 | {data.title}
26 | Created {data.createdAt.toLocaleDateString('en-us')}
27 |
28 | {data.text}
29 |
30 | Raw data:
31 | {JSON.stringify(data, null, 4)}
32 | >
33 | );
34 | };
35 |
36 | export default PostViewPage;
37 |
--------------------------------------------------------------------------------
/src/server/context.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import * as trpc from '@trpc/server';
3 | import * as trpcNext from '@trpc/server/adapters/next';
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
6 | interface CreateContextOptions {
7 | // session: Session | null
8 | }
9 |
10 | /**
11 | * Inner function for `createContext` where we create the context.
12 | * This is useful for testing when we don't want to mock Next.js' request/response
13 | */
14 | export async function createContextInner(_opts: CreateContextOptions) {
15 | return {};
16 | }
17 |
18 | export type Context = trpc.inferAsyncReturnType;
19 |
20 | /**
21 | * Creates context for an incoming request
22 | * @link https://trpc.io/docs/context
23 | */
24 | export async function createContext(
25 | opts: trpcNext.CreateNextContextOptions,
26 | ): Promise {
27 | // for API-response caching see https://trpc.io/docs/caching
28 |
29 | return await createContextInner({});
30 | }
31 |
--------------------------------------------------------------------------------
/src/server/env.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | /**
3 | * This file is included in `/next.config.js` which ensures the app isn't built with invalid env vars.
4 | * It has to be a `.js`-file to be imported there.
5 | */
6 | /* eslint-disable @typescript-eslint/no-var-requires */
7 | const { z } = require('zod');
8 |
9 | /*eslint sort-keys: "error"*/
10 | const envSchema = z.object({
11 | // DATABASE_URL: z.string().url(),
12 | NODE_ENV: z.enum(['development', 'test', 'production']),
13 | });
14 |
15 | const env = envSchema.safeParse(process.env);
16 |
17 | if (!env.success) {
18 | console.error(
19 | '❌ Invalid environment variables:',
20 | JSON.stringify(env.error.format(), null, 4),
21 | );
22 | process.exit(1);
23 | }
24 | module.exports.env = env.data;
25 |
--------------------------------------------------------------------------------
/src/server/prisma.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Instantiates a single instance PrismaClient and save it on the global object.
3 | * @link https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices
4 | */
5 | import { env } from './env';
6 | import { PrismaClient } from '@prisma/client';
7 |
8 | const prismaGlobal = global as typeof global & {
9 | prisma?: PrismaClient;
10 | };
11 |
12 | export const prisma: PrismaClient =
13 | prismaGlobal.prisma ||
14 | new PrismaClient({
15 | log:
16 | env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
17 | });
18 |
19 | if (env.NODE_ENV !== 'production') {
20 | prismaGlobal.prisma = prisma;
21 | }
22 |
--------------------------------------------------------------------------------
/src/server/routers/_app.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains the root router of your tRPC-backend
3 | */
4 | import { router } from '../trpc';
5 | import { healthRouter } from './health';
6 | import { postRouter } from './post';
7 |
8 | export const appRouter = router({
9 | post: postRouter,
10 | health: healthRouter,
11 | });
12 |
13 | export type AppRouter = typeof appRouter;
14 |
--------------------------------------------------------------------------------
/src/server/routers/health.ts:
--------------------------------------------------------------------------------
1 | import { router, baseProcedure } from '../trpc';
2 |
3 | export const healthRouter = router({
4 | healthz: baseProcedure.query(() => 'yay!'),
5 | });
6 |
--------------------------------------------------------------------------------
/src/server/routers/post.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Integration test example for the `post` router
3 | */
4 | import { createContextInner } from '../context';
5 | import { AppRouter, appRouter } from './_app';
6 | import { inferProcedureInput } from '@trpc/server';
7 |
8 | test('add and get post', async () => {
9 | const ctx = await createContextInner({});
10 | const caller = appRouter.createCaller(ctx);
11 |
12 | const input: inferProcedureInput = {
13 | text: 'hello test',
14 | title: 'hello test',
15 | };
16 |
17 | const post = await caller.post.add(input);
18 | const byId = await caller.post.byId({ id: post.id });
19 |
20 | expect(byId).toMatchObject(input);
21 | });
22 |
--------------------------------------------------------------------------------
/src/server/routers/post.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx`
4 | */
5 | import { router, baseProcedure } from '../trpc';
6 | import { Prisma } from '@prisma/client';
7 | import { TRPCError } from '@trpc/server';
8 | import { z } from 'zod';
9 | import { prisma } from '~/server/prisma';
10 |
11 | /**
12 | * Default selector for Post.
13 | * It's important to always explicitly say which fields you want to return in order to not leak extra information
14 | * @see https://github.com/prisma/prisma/issues/9353
15 | */
16 | const defaultPostSelect = Prisma.validator()({
17 | id: true,
18 | title: true,
19 | text: true,
20 | createdAt: true,
21 | updatedAt: true,
22 | });
23 |
24 | export const postRouter = router({
25 | list: baseProcedure.query(() => {
26 | /**
27 | * For pagination you can have a look at this docs site
28 | * @link https://trpc.io/docs/useInfiniteQuery
29 | */
30 |
31 | return prisma.post.findMany({
32 | select: defaultPostSelect,
33 | });
34 | }),
35 | byId: baseProcedure
36 | .input(
37 | z.object({
38 | id: z.string(),
39 | }),
40 | )
41 | .query(async ({ input }) => {
42 | const { id } = input;
43 | const post = await prisma.post.findUnique({
44 | where: { id },
45 | select: defaultPostSelect,
46 | });
47 | if (!post) {
48 | throw new TRPCError({
49 | code: 'NOT_FOUND',
50 | message: `No post with id '${id}'`,
51 | });
52 | }
53 | return post;
54 | }),
55 | add: baseProcedure
56 | .input(
57 | z.object({
58 | id: z.string().uuid().optional(),
59 | title: z.string().min(1).max(32),
60 | text: z.string().min(1),
61 | }),
62 | )
63 | .mutation(async ({ input }) => {
64 | const post = await prisma.post.create({
65 | data: input,
66 | select: defaultPostSelect,
67 | });
68 | return post;
69 | }),
70 | });
71 |
--------------------------------------------------------------------------------
/src/server/trpc.ts:
--------------------------------------------------------------------------------
1 | import { Context } from './context';
2 | import { initTRPC } from '@trpc/server';
3 | import superjson from 'superjson';
4 |
5 | const t = initTRPC.context().create({
6 | transformer: superjson,
7 | errorFormatter({ shape }) {
8 | return shape;
9 | },
10 | });
11 |
12 | export const router = t.router;
13 | export const baseProcedure = t.procedure;
14 |
--------------------------------------------------------------------------------
/src/utils/publicRuntimeConfig.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Dynamic configuration available for the browser and server populated from your `next.config.js`.
3 | * Note: requires `ssr: true` or a `getInitialProps` in `_app.tsx`
4 | * @link https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration
5 | */
6 | import type * as config from '../../next.config';
7 | import getConfig from 'next/config';
8 |
9 | /**
10 | * Inferred type from `publicRuntime` in `next.config.js`
11 | */
12 | type PublicRuntimeConfig = typeof config.publicRuntimeConfig;
13 |
14 | const nextConfig = getConfig();
15 |
16 | export const publicRuntimeConfig =
17 | nextConfig.publicRuntimeConfig as PublicRuntimeConfig;
18 |
--------------------------------------------------------------------------------
/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | import { httpBatchLink, loggerLink } from '@trpc/client';
2 | import { createTRPCNext } from '@trpc/next';
3 | import { NextPageContext } from 'next';
4 | import superjson from 'superjson';
5 | // ℹ️ Type-only import:
6 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
7 | import type { AppRouter } from '~/server/routers/_app';
8 |
9 | function getBaseUrl() {
10 | if (typeof window !== 'undefined') {
11 | return '';
12 | }
13 | // reference for vercel.com
14 | if (process.env.VERCEL_URL) {
15 | return `https://${process.env.VERCEL_URL}`;
16 | }
17 |
18 | // // reference for render.com
19 | if (process.env.RENDER_INTERNAL_HOSTNAME) {
20 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
21 | }
22 |
23 | // assume localhost
24 | return `http://localhost:${process.env.PORT ?? 3000}`;
25 | }
26 |
27 | /**
28 | * Extend `NextPageContext` with meta data that can be picked up by `responseMeta()` when server-side rendering
29 | */
30 | export interface SSRContext extends NextPageContext {
31 | /**
32 | * Set HTTP Status code
33 | * @example
34 | * const utils = trpc.useContext();
35 | * if (utils.ssrContext) {
36 | * utils.ssrContext.status = 404;
37 | * }
38 | */
39 | status?: number;
40 | }
41 |
42 | /**
43 | * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
44 | * @link https://trpc.io/docs/react#3-create-trpc-hooks
45 | */
46 | export const trpc = createTRPCNext({
47 | config() {
48 | /**
49 | * If you want to use SSR, you need to use the server's full URL
50 | * @link https://trpc.io/docs/ssr
51 | */
52 | return {
53 | /**
54 | * @link https://trpc.io/docs/data-transformers
55 | */
56 | transformer: superjson,
57 | /**
58 | * @link https://trpc.io/docs/links
59 | */
60 | links: [
61 | // adds pretty logs to your console in development and logs errors in production
62 | loggerLink({
63 | enabled: (opts) =>
64 | process.env.NODE_ENV === 'development' ||
65 | (opts.direction === 'down' && opts.result instanceof Error),
66 | }),
67 |
68 | httpBatchLink({
69 | url: `${getBaseUrl()}/api/trpc`,
70 | }),
71 | ],
72 | /**
73 | * @link https://react-query.tanstack.com/reference/QueryClient
74 | */
75 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
76 | };
77 | },
78 | /**
79 | * @link https://trpc.io/docs/ssr
80 | */
81 | ssr: true,
82 | /**
83 | * Set headers or status code when doing SSR
84 | */
85 | responseMeta(opts) {
86 | const ctx = opts.ctx as SSRContext;
87 |
88 | if (ctx.status) {
89 | // If HTTP status set, propagate that
90 | return {
91 | status: ctx.status,
92 | };
93 | }
94 |
95 | const error = opts.clientErrors[0];
96 | if (error) {
97 | // Propagate http first error from API calls
98 | return {
99 | status: error.data?.httpStatus ?? 500,
100 | };
101 | }
102 |
103 | // for app caching with SSR see https://trpc.io/docs/caching
104 |
105 | return {};
106 | },
107 | });
108 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "strictNullChecks": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "baseUrl": ".",
19 | "paths": {
20 | "~/*": ["src/*"]
21 | }
22 | },
23 | "include": [
24 | "next-env.d.ts",
25 | "**/*.ts",
26 | "**/*.tsx",
27 | "./*.js",
28 | "./src/**/*.js"
29 | ],
30 | "exclude": ["node_modules"]
31 | }
32 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'url';
2 | import { configDefaults, defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | test: {
6 | globals: true,
7 | exclude: [...configDefaults.exclude, '**/playwright/**'],
8 | alias: {
9 | '~/': fileURLToPath(new URL('./src/', import.meta.url)),
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------