├── sandbox.config.json ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── main.yml │ └── codeql-analysis.yml ├── public ├── favicon.ico └── vercel.svg ├── src ├── server │ ├── lib │ │ ├── sum.ts │ │ └── sum.test.ts │ └── trpc │ │ ├── routers │ │ ├── source.ts │ │ └── index.ts │ │ ├── trpc.ts │ │ └── context.ts ├── pages │ ├── next-auth.tsx │ ├── react-hook-form.tsx │ ├── api │ │ ├── trpc │ │ │ └── [trpc].ts │ │ └── auth │ │ │ └── [...nextauth].ts │ ├── ssg.tsx │ ├── index.tsx │ └── _app.tsx ├── feature │ ├── ssg │ │ ├── meta.tsx │ │ └── router.ts │ ├── react-hook-form │ │ ├── router.ts │ │ ├── meta.tsx │ │ ├── Form.tsx │ │ └── index.tsx │ └── next-auth │ │ ├── router.tsx │ │ ├── meta.tsx │ │ └── index.tsx ├── utils │ ├── useClipboard.ts │ ├── ClientSuspense.tsx │ ├── trpc.ts │ └── ExamplePage.tsx └── styles │ └── globals.css ├── postcss.config.js ├── .vscode ├── settings.json └── extensions.json ├── next.config.js ├── README.md ├── .env-example ├── next-env.d.ts ├── docker-compose.yaml ├── .kodiak.toml ├── tailwind.config.js ├── .gitignore ├── tsconfig.json ├── jest.config.ts ├── .eslintrc └── package.json /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "next" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: KATT 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trpc/examples-kitchen-sink/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/server/lib/sum.ts: -------------------------------------------------------------------------------- 1 | export function sum(a: number, b: number) { 2 | return a + b; 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/server/lib/sum.test.ts: -------------------------------------------------------------------------------- 1 | import { sum } from './sum'; 2 | 3 | test('sum', () => { 4 | expect(sum(1, 1)).toBe(2); 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "typescript.tsdk": "node_modules/typescript/lib" 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://nextjs.org/docs/api-reference/next.config.js/introduction 3 | */ 4 | module.exports = { 5 | reactStrictMode: true, 6 | }; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tRPC Kitchen Sink 2 | 3 | https://kitchen-sink.trpc.io/ 4 | 5 | 6 | ## Wanna Contribute? 🙏 7 | 8 | See [#1254](https://github.com/trpc/trpc/issues/1254) 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "Orta.vscode-twoslash-queries" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://postgres:@localhost:5792/trpcdb 2 | NEXTAUTH_URL=http://localhost:3000 3 | NEXTAUTH_SECRET=changeme 4 | NEXT_PUBLIC_APP_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/pages/next-auth.tsx: -------------------------------------------------------------------------------- 1 | import FeaturePage from 'feature/next-auth'; 2 | import { meta } from 'feature/next-auth/meta'; 3 | import { ExamplePage } from 'utils/ExamplePage'; 4 | 5 | export default function Page() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/react-hook-form.tsx: -------------------------------------------------------------------------------- 1 | import FeaturePage from 'feature/react-hook-form'; 2 | import { meta } from 'feature/react-hook-form/meta'; 3 | import { ExamplePage } from 'utils/ExamplePage'; 4 | 5 | export default function Page() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | postgres: 4 | image: postgres:13 5 | ports: 6 | - '5792:5432' # expose pg on port 5632 to not collide with pg from elswhere 7 | restart: always 8 | volumes: 9 | - db_data:/var/lib/postgresql/data 10 | environment: 11 | POSTGRES_PASSWORD: ${PGPASSWORD} 12 | POSTGRES_HOST_AUTH_METHOD: trust 13 | volumes: 14 | db_data: 15 | -------------------------------------------------------------------------------- /.kodiak.toml: -------------------------------------------------------------------------------- 1 | # .kodiak.toml 2 | version = 1 3 | 4 | [approve] 5 | auto_approve_usernames = ["dependabot"] 6 | 7 | [merge] 8 | method = "squash" 9 | automerge_label = ["🚀 merge", "⬆️ dependencies"] 10 | 11 | [merge.automerge_dependencies] 12 | # only auto merge "minor" and "patch" version upgrades. 13 | versions = ["minor", "patch"] 14 | usernames = ["dependabot"] 15 | 16 | [update] 17 | autoupdate_label = "♻️ autoupdate" 18 | -------------------------------------------------------------------------------- /src/feature/ssg/meta.tsx: -------------------------------------------------------------------------------- 1 | import { ExampleProps } from 'utils/ExamplePage'; 2 | 3 | export const meta: ExampleProps = { 4 | title: 'Static Site Generation (SSG)', 5 | href: '/ssg', 6 | summary: ( 7 | <> 8 |

9 | Using Static Site Generation & getStaticProps 10 |

11 | 12 | ), 13 | files: [ 14 | { title: 'Router', path: 'feature/ssg/router.ts' }, 15 | { title: 'Page', path: 'pages/ssg.tsx' }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/feature/ssg/router.ts: -------------------------------------------------------------------------------- 1 | import { t } from 'server/trpc/trpc'; 2 | import { z } from 'zod'; 3 | 4 | const posts = [ 5 | { 6 | id: '1', 7 | title: 'This data comes from the backend', 8 | }, 9 | ]; 10 | 11 | const ssgRouter = t.router({ 12 | byId: t.procedure 13 | .input( 14 | z.object({ 15 | id: z.string(), 16 | }), 17 | ) 18 | .query(async ({ input }) => { 19 | const post = posts.find((post) => post.id === input.id); 20 | return post ?? null; 21 | }), 22 | }); 23 | 24 | export default ssgRouter; 25 | -------------------------------------------------------------------------------- /src/utils/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useClipboard = (text: string): [boolean, () => void] => { 4 | const [hasCopied, setHasCopied] = useState(false); 5 | 6 | useEffect(() => { 7 | if (hasCopied) { 8 | const id = setTimeout(() => { 9 | setHasCopied(false); 10 | }, 600); 11 | 12 | return () => { 13 | clearTimeout(id); 14 | }; 15 | } 16 | }, [hasCopied]); 17 | 18 | return [ 19 | hasCopied, 20 | () => { 21 | navigator.clipboard.writeText(text).then(() => setHasCopied(true)); 22 | }, 23 | ]; 24 | }; 25 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{js,ts,jsx,tsx}'], 3 | theme: { 4 | extend: { 5 | colors: { 6 | primary: { 7 | 100: '#e8f0f9', 8 | 200: '#bbd3ee', 9 | 300: '#8db6e3', 10 | 400: '#337ccc', 11 | 500: '#3178c6', 12 | 600: '#27609f', 13 | 700: '#1c4572', 14 | 800: '#112944', 15 | 900: '#060e17', 16 | }, 17 | }, 18 | }, 19 | }, 20 | variants: { 21 | extend: {}, 22 | }, 23 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')], 24 | }; 25 | -------------------------------------------------------------------------------- /.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 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | *.db 38 | *.db-journal 39 | 40 | 41 | # testing 42 | playwright/videos 43 | playwright/screenshots 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 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 | "types": ["@types/jest"], 18 | "baseUrl": "./src", 19 | "incremental": true 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /src/server/trpc/routers/source.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { z } from 'zod'; 4 | 5 | import { t } from '../trpc'; 6 | 7 | export const sourceRouter = t.router({ 8 | getSource: t.procedure 9 | .input( 10 | z.object({ 11 | path: z.string().refine((val) => !val.includes('..'), { 12 | message: 'Only relative paths allowed', 13 | }), 14 | }), 15 | ) 16 | .query(async ({ input }) => { 17 | const ROOT = path.resolve(__dirname + '/../../../../../src') + '/'; 18 | const contents = fs.readFileSync(ROOT + input.path).toString('utf8'); 19 | 20 | return { 21 | contents, 22 | }; 23 | }), 24 | }); 25 | -------------------------------------------------------------------------------- /src/feature/react-hook-form/router.ts: -------------------------------------------------------------------------------- 1 | import { t } from 'server/trpc/trpc'; 2 | 3 | import { validationSchema } from './index'; 4 | 5 | const items = [ 6 | { 7 | id: '1', 8 | title: 'Hello tRPC', 9 | text: 'Hello world', 10 | }, 11 | ]; 12 | 13 | export const reactHookFormRouter = t.router({ 14 | list: t.procedure.query(async () => { 15 | return items; 16 | }), 17 | 18 | add: t.procedure.input(validationSchema).mutation(({ input }) => { 19 | const id = Math.random() 20 | .toString(36) 21 | .replace(/[^a-z]+/g, '') 22 | .slice(0, 6); 23 | const item = { 24 | id, 25 | ...input, 26 | }; 27 | items.push(item); 28 | 29 | return item; 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | verbose: true, 5 | roots: [''], 6 | testMatch: [ 7 | '**/tests/**/*.+(ts|tsx|js)', 8 | '**/?(*.)+(spec|test).+(ts|tsx|js)', 9 | ], 10 | testPathIgnorePatterns: ['/.next', '/playwright/'], 11 | transform: { 12 | '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }], 13 | }, 14 | transformIgnorePatterns: [ 15 | '/node_modules/', 16 | '^.+\\.module\\.(css|sass|scss)$', 17 | ], 18 | testEnvironment: 'jsdom', 19 | moduleNameMapper: { 20 | '^@components(.*)$': '/components$1', 21 | '^@lib(.*)$': '/lib$1', 22 | }, 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /src/server/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server'; 2 | import superjson from 'superjson'; 3 | 4 | import type { Context } from './context'; 5 | 6 | export const t = initTRPC.context().create({ 7 | /** 8 | * SuperJSON allows us to transparently use e.g. standard Date/Map/Sets over the wire 9 | * between the server and client. That means you can return any of these types in your 10 | * API-resolver and use them in the client without recreating the objects from JSON. 11 | * https://trpc.io/docs/v10/data-transformers#using-superjson 12 | */ 13 | transformer: superjson, 14 | /** 15 | * Optionally do custom error (type safe!) formatting 16 | * https://trpc.io/docs/v10/error-formatting 17 | */ 18 | errorFormatter({ shape }) { 19 | return shape; 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/utils/ClientSuspense.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactNode } from 'react'; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | 7 | interface State { 8 | hasError: boolean; 9 | } 10 | 11 | export class ErrorBoundary extends Component { 12 | public state: State = { 13 | hasError: false, 14 | }; 15 | 16 | public static getDerivedStateFromError(): State { 17 | // Update state so the next render will show the fallback UI. 18 | return { hasError: true }; 19 | } 20 | 21 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) { 22 | console.error('Uncaught error:', error, errorInfo); 23 | } 24 | 25 | public render() { 26 | if (this.state.hasError) { 27 | return

Sorry.. there was an error

; 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html { 6 | scroll-behavior: smooth; 7 | } 8 | body { 9 | @apply bg-primary-100; 10 | } 11 | 12 | .btn { 13 | @apply relative inline-flex items-center px-3 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-500 transition ease-in-out duration-150 rounded-md; 14 | } 15 | .btn__icon { 16 | @apply mr-2 h-5 w-5 transition-colors opacity-90; 17 | } 18 | 19 | .btn--active { 20 | @apply bg-primary-500 text-white; 21 | } 22 | 23 | .btn:focus { 24 | @apply z-10 outline-none border-primary-300; 25 | } 26 | 27 | .btn-group { 28 | @apply relative z-0 inline-flex shadow-sm; 29 | } 30 | 31 | .btn-group > .btn:not(:first-child) { 32 | @apply rounded-l-none -ml-px; 33 | } 34 | 35 | .btn-group > .btn:not(:last-child) { 36 | @apply rounded-r-none; 37 | } 38 | -------------------------------------------------------------------------------- /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/trpc/context'; 6 | import { appRouter } from 'server/trpc/routers'; 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/server/trpc/routers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the root router of your tRPC-backend 3 | */ 4 | import { authRouter } from 'feature/next-auth/router'; 5 | import { reactHookFormRouter } from 'feature/react-hook-form/router'; 6 | import ssgRouter from 'feature/ssg/router'; 7 | 8 | import { t } from '../trpc'; 9 | import { sourceRouter } from './source'; 10 | 11 | /** 12 | * In tRPC v10 the root router is created by the same function as child 13 | * routers, and they can be nested arbitrarily. 14 | * The root router gets passed to `createNextApiHandler` to handle routing in /api/trpc 15 | * The root router's type gets passed to `createTRPCNext` so the frontend knows the routes/schema/returns 16 | */ 17 | export const appRouter = t.router({ 18 | healthcheck: t.procedure.query(() => 'ok'), 19 | 20 | source: sourceRouter, 21 | ssgRouter: ssgRouter, 22 | authRouter: authRouter, 23 | reactHookFormRouter: reactHookFormRouter, 24 | }); 25 | export type AppRouter = typeof appRouter; 26 | -------------------------------------------------------------------------------- /src/feature/next-auth/router.tsx: -------------------------------------------------------------------------------- 1 | import { TRPCError } from '@trpc/server'; 2 | import { t } from 'server/trpc/trpc'; 3 | 4 | const authMiddleware = t.middleware(async ({ ctx, next }) => { 5 | // any query that uses this middleware will throw 6 | // an error unless there is a current session 7 | if (!ctx.session) { 8 | throw new TRPCError({ code: 'UNAUTHORIZED' }); 9 | } 10 | return next(); 11 | }); 12 | 13 | // you can create a named procedure that uses a middleware 14 | // (as is done here), 15 | // or just use the middleware inline in the router like: 16 | // `someProcedure: t.procedure.use(someMiddleware).query() 17 | const authedProcedure = t.procedure.use(authMiddleware); 18 | 19 | export const authRouter = t.router({ 20 | getSession: t.procedure.query(({ ctx }) => { 21 | // The session object is added to the routers context 22 | // in the context file server side 23 | return ctx.session; 24 | }), 25 | getSecretCode: authedProcedure.query(async () => { 26 | const secretCode = 'the cake is a lie.'; 27 | return secretCode; 28 | }), 29 | }); 30 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/feature/next-auth/meta.tsx: -------------------------------------------------------------------------------- 1 | import { ExampleProps } from 'utils/ExamplePage'; 2 | 3 | export const meta: ExampleProps = { 4 | title: 'Next-Auth Authentication', 5 | href: '/next-auth', 6 | // This is only enabled on the client as it will cause hydration error otherwise 7 | // The problem is that RSC won't send the right header whilst the client will, leading to inconsistent behavior 8 | clientOnly: true, 9 | summary: ( 10 | <> 11 |

12 | Using tRPC & NextAuth 13 |

14 | 15 | ), 16 | detail: ( 17 | <> 18 |

19 | Using tRPC &{' '} 20 | 26 | NextAuth 27 | 28 | . 29 |

30 | 31 | ), 32 | files: [ 33 | { title: 'Page', path: 'feature/next-auth/index.tsx' }, 34 | { title: 'Router', path: 'feature/next-auth/router.tsx' }, 35 | { title: 'Context', path: 'server/trpc/context.ts' }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /src/feature/react-hook-form/meta.tsx: -------------------------------------------------------------------------------- 1 | import { ExampleProps } from 'utils/ExamplePage'; 2 | 3 | export const meta: ExampleProps = { 4 | title: 'React Hook Form', 5 | href: '/react-hook-form', 6 | clientOnly: true, 7 | summary: ( 8 | <> 9 |

10 | Using tRPC with react-hook-form. 11 |

12 | 13 | ), 14 | detail: ( 15 | <> 16 |

17 | Using tRPC &{' '} 18 | 24 | react-hook-form. 25 | 26 |
27 | Note how the same 28 | zod 29 | {' '} 30 | validation schema is reused both for the client & the server. 31 |

32 | 33 | ), 34 | files: [ 35 | { title: 'Router', path: 'feature/react-hook-form/router.ts' }, 36 | { title: 'Page', path: 'feature/react-hook-form/index.tsx' }, 37 | { title: 'Form utils', path: 'feature/react-hook-form/Form.tsx' }, 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /src/server/trpc/context.ts: -------------------------------------------------------------------------------- 1 | import * as trpc from '@trpc/server'; 2 | import * as trpcNext from '@trpc/server/adapters/next'; 3 | import { unstable_getServerSession } from 'next-auth'; 4 | 5 | import { authOptions as nextAuthOptions } from '../../pages/api/auth/[...nextauth]'; 6 | 7 | /** 8 | * Creates context for an incoming request 9 | * @link https://trpc.io/docs/context 10 | */ 11 | export const createContext = async ( 12 | opts?: trpcNext.CreateNextContextOptions, 13 | ) => { 14 | const req = opts?.req; 15 | const res = opts?.res; 16 | 17 | /** 18 | * Uses faster "unstable_getServerSession" in next-auth v4 that avoids 19 | * a fetch request to /api/auth. 20 | * This function also updates the session cookie whereas getSession does not 21 | * Note: If no req -> SSG is being used -> no session exists (null) 22 | * @link https://github.com/nextauthjs/next-auth/issues/1535 23 | */ 24 | // const session = opts && (await getServerSession(opts, nextAuthOptions)); 25 | const session = 26 | req && res && (await unstable_getServerSession(req, res, nextAuthOptions)); 27 | 28 | // for API-response caching see https://trpc.io/docs/caching 29 | return { 30 | req, 31 | res, 32 | session, 33 | }; 34 | }; 35 | 36 | export type Context = trpc.inferAsyncReturnType; 37 | -------------------------------------------------------------------------------- /.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 | "ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features 11 | "sourceType": "module" // Allows for the use of imports 12 | }, 13 | "plugins": ["simple-import-sort"], 14 | "rules": { 15 | // Auto-sort imports and exports 16 | "simple-import-sort/imports": "error", 17 | "simple-import-sort/exports": "error", 18 | "sort-imports": "off", 19 | "import/order": "off", 20 | 21 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 22 | "@typescript-eslint/explicit-function-return-type": "off", 23 | "@typescript-eslint/explicit-module-boundary-types": "off", 24 | "react/react-in-jsx-scope": "off", 25 | "react/prop-types": "off", 26 | "@typescript-eslint/no-explicit-any": "off" 27 | }, 28 | // "overrides": [ 29 | // { 30 | // "files": [], 31 | // "rules": { 32 | // "@typescript-eslint/no-unused-vars": "off" 33 | // } 34 | // } 35 | // ], 36 | "settings": { 37 | "react": { 38 | "version": "detect" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/ssg.tsx: -------------------------------------------------------------------------------- 1 | import { createServerSideHelpers } from '@trpc/react-query/server'; 2 | import { meta } from 'feature/ssg/meta'; 3 | import { InferGetStaticPropsType } from 'next'; 4 | import { createContext } from 'server/trpc/context'; 5 | import { appRouter } from 'server/trpc/routers'; 6 | import superjson from 'superjson'; 7 | import { ExamplePage } from 'utils/ExamplePage'; 8 | import { trpc } from 'utils/trpc'; 9 | 10 | export default function Page( 11 | props: InferGetStaticPropsType, 12 | ) { 13 | const { id } = props; 14 | const query = trpc.ssgRouter.byId.useQuery({ id }); 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 17 | const post = query.data!; 18 | 19 | return ( 20 | <> 21 | 22 |
23 |

{post.title}

24 |
25 |
26 | 27 | ); 28 | } 29 | 30 | export async function getStaticProps() { 31 | const ssgHelper = createServerSideHelpers({ 32 | router: appRouter, 33 | ctx: await createContext(), 34 | transformer: superjson, // optional - adds superjson serialization 35 | }); 36 | 37 | const id = '1'; 38 | const post = await ssgHelper.ssgRouter.byId.fetch({ id }); 39 | 40 | if (!post) { 41 | return { 42 | notFound: true, 43 | }; 44 | } 45 | return { 46 | props: { 47 | trpcState: ssgHelper.dehydrate(), 48 | id, 49 | }, 50 | revalidate: 1, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: E2E-testing 2 | on: [push] 3 | jobs: 4 | e2e: 5 | env: 6 | DATABASE_URL: postgresql://postgres:@localhost:5432/trpcdb 7 | NEXTAUTH_SECRET: test 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | node: ['14.x'] 12 | os: [ubuntu-latest] 13 | services: 14 | postgres: 15 | image: postgres:12.1 16 | env: 17 | POSTGRES_USER: postgres 18 | POSTGRES_DB: trpcdb 19 | ports: 20 | - 5432:5432 21 | steps: 22 | - name: Checkout repo 23 | uses: actions/checkout@v2 24 | 25 | - uses: pnpm/action-setup@v2.2.4 26 | 27 | - name: Get pnpm store directory 28 | id: pnpm-cache 29 | run: | 30 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 31 | 32 | - uses: actions/cache@v3 33 | name: Setup pnpm cache 34 | with: 35 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 36 | key: ${{ runner.os }}-pnpm-store-${{ matrix.dir }}-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pnpm-store-${{ matrix.dir }}- 39 | 40 | - run: node -v 41 | - name: Install deps (with cache) 42 | run: pnpm install 43 | 44 | - name: Next.js cache 45 | uses: actions/cache@v2 46 | with: 47 | path: ${{ github.workspace }}/.next/cache 48 | key: ${{ runner.os }}-${{ runner.node }}-${{ hashFiles('**/pnpm.lock') }}-nextjs 49 | 50 | - run: pnpm lint 51 | - run: pnpm build 52 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { NextAuthOptions, User } from 'next-auth'; 2 | import CredentialsProvider from 'next-auth/providers/credentials'; 3 | 4 | export const authOptions: NextAuthOptions = { 5 | providers: [ 6 | CredentialsProvider({ 7 | // The name to display on the sign in form (e.g. 'Sign in with...') 8 | name: 'Next Auth', 9 | // The credentials is used to generate a suitable form on the sign in page. 10 | // You can specify whatever fields you are expecting to be submitted. 11 | // e.g. domain, username, password, 2FA token, etc. 12 | // You can pass any HTML attribute to the tag through the object. 13 | credentials: { 14 | username: { 15 | label: 'Username', 16 | type: 'text', 17 | placeholder: 'Any credentials work', 18 | }, 19 | password: { label: 'Password', type: 'password' }, 20 | }, 21 | async authorize(credentials) { 22 | // You need to provide your own logic here that takes the credentials 23 | // submitted and returns either a object representing a user or value 24 | // that is false/null if the credentials are invalid. 25 | // e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' } 26 | // You can also use the `req` object to obtain additional parameters 27 | // (i.e., the request IP address) 28 | 29 | const user: User = { 30 | id: '1', 31 | name: 'J Smith', 32 | email: credentials?.username, 33 | }; 34 | 35 | return user; 36 | }, 37 | }), 38 | ], 39 | secret: process.env.NEXTAUTH_SECRET, 40 | }; 41 | 42 | export default NextAuth(authOptions); 43 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { meta as nextAuthMeta } from 'feature/next-auth/meta'; 2 | import { meta as reactHookFormMeta } from 'feature/react-hook-form/meta'; 3 | import { meta as ssgMeta } from 'feature/ssg/meta'; 4 | import Head from 'next/head'; 5 | import Link from 'next/link'; 6 | import { ExampleProps } from 'utils/ExamplePage'; 7 | 8 | const propsList: ExampleProps[] = [reactHookFormMeta, ssgMeta, nextAuthMeta]; 9 | 10 | export default function Page() { 11 | return ( 12 | <> 13 | 14 | tRPC Kitchen Sink 15 | 16 |
17 |
18 |

19 | A collection tRPC usage patterns 20 |

21 |

22 | Your go-to place to find out how to find solutions to common 23 | problems. 24 |

25 |
26 |
27 |
28 | 40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | 3 | import { AppType } from 'next/dist/shared/lib/utils'; 4 | import { useEffect, useState } from 'react'; 5 | import { trpc } from 'utils/trpc'; 6 | 7 | function ContributorsWantedBanner() { 8 | const [visible, setVisible] = useState(false); 9 | useEffect(() => { 10 | setTimeout(() => { 11 | setVisible(true); 12 | }, 3e3); 13 | }, []); 14 | 15 | return ( 16 | <> 17 |
18 |
24 |
25 |
26 |
27 |

28 | 29 | Contributors wanted to improve this page & to add more 30 | examples! 31 | 32 | 33 | 37 | {' '} 38 | Learn more 39 | 40 | 41 |

42 |
43 |
44 |
45 |
46 | 47 | ); 48 | } 49 | 50 | const MyApp: AppType = (props) => { 51 | return ( 52 | <> 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default trpc.withTRPC(MyApp); 60 | -------------------------------------------------------------------------------- /src/feature/next-auth/index.tsx: -------------------------------------------------------------------------------- 1 | import { signIn, signOut } from 'next-auth/react'; 2 | import { trpc } from 'utils/trpc'; 3 | 4 | export default function NextAuth() { 5 | return ( 6 | <> 7 |

Next Auth Examples

8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | function ServerSideSessionCheck() { 16 | const [session] = trpc.authRouter.getSession.useSuspenseQuery(); 17 | 18 | return ( 19 |
20 |

21 | Server side session check with tRPC's context 22 |

23 | {session ? ( 24 | <> 25 | Signed in as {session?.user?.email}
26 | 27 | ) : ( 28 | <> 29 | Not signed in
30 | 31 | )} 32 |
33 | ); 34 | } 35 | 36 | function MiddlewareQuery() { 37 | const query = trpc.authRouter.getSecretCode.useQuery(); 38 | 39 | const secretCode = query.data; 40 | return ( 41 |
42 |

43 | Server side middleware session check with tRPC's context 44 |

45 | {secretCode ? ( 46 | <> 47 | You're logged in. The secret code from the server is: " 48 | {secretCode}" 49 |
50 | 51 | ) : ( 52 | <> 53 | Not signed in, no code from the server, a 401 response and an error is 54 | raised.
55 | 56 | )} 57 |
58 | ); 59 | } 60 | 61 | function SignInButton() { 62 | const [session] = trpc.authRouter.getSession.useSuspenseQuery(); 63 | 64 | return ( 65 |
66 | 80 |

(Any credentials work)

81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@examples/kitchen-sink", 3 | "version": "9.14.0", 4 | "private": true, 5 | "packageManager": "pnpm@7.18.2", 6 | "scripts": { 7 | "build": "next build", 8 | "dev": "next dev", 9 | "start": "next start", 10 | "lint": "eslint src", 11 | "lint-fix": "pnpm lint --fix", 12 | "test-dev": "start-server-and-test dev 3000 test", 13 | "test-start": "start-server-and-test start 3000 test", 14 | "ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\"" 15 | }, 16 | "prettier": { 17 | "printWidth": 80, 18 | "trailingComma": "all", 19 | "singleQuote": true 20 | }, 21 | "dependencies": { 22 | "@heroicons/react": "^1.0.6", 23 | "@hookform/resolvers": "^3.1.1", 24 | "@tailwindcss/typography": "^0.5.7", 25 | "@tanstack/react-query": "^4.2.3", 26 | "@trpc/client": "^10.34.0", 27 | "@trpc/next": "^10.34.0", 28 | "@trpc/react-query": "^10.34.0", 29 | "@trpc/server": "^10.34.0", 30 | "autoprefixer": "^10.4.11", 31 | "clsx": "^1.1.1", 32 | "next": "12.3.1", 33 | "next-auth": "^4.10.3", 34 | "prism-react-renderer": "^1.3.3", 35 | "react": "^18.2.0", 36 | "react-dom": "^18.2.0", 37 | "react-hook-form": "^7.45.2", 38 | "start-server-and-test": "^1.12.0", 39 | "superjson": "^1.9.1", 40 | "tailwindcss": "^3.1.8", 41 | "zod": "^3.16.0" 42 | }, 43 | "devDependencies": { 44 | "@tailwindcss/forms": "^0.5.4", 45 | "@types/jest": "^29.2.4", 46 | "@types/node": "^17.0.33", 47 | "@types/react": "^18.0.9", 48 | "@typescript-eslint/eslint-plugin": "^4.30.0", 49 | "@typescript-eslint/parser": "^4.26.0", 50 | "cross-env": "^7.0.3", 51 | "eslint": "^7.32.0", 52 | "eslint-config-next": "^12.3.1", 53 | "eslint-config-prettier": "^8.5.0", 54 | "eslint-plugin-prettier": "^4.0.0", 55 | "eslint-plugin-react": "^7.29.4", 56 | "eslint-plugin-react-hooks": "^4.5.0", 57 | "eslint-plugin-simple-import-sort": "^7.0.0", 58 | "jest": "^29.3.1", 59 | "npm-run-all": "^4.1.5", 60 | "postcss": "^8.4.16", 61 | "prettier": "^2.6.2", 62 | "ts-jest": "^29.0.3", 63 | "ts-node": "^10.7.0", 64 | "typescript": "4.7.4" 65 | }, 66 | "publishConfig": { 67 | "access": "restricted" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { httpBatchLink, loggerLink } from '@trpc/client'; 2 | import { createTRPCNext } from '@trpc/next'; 3 | // ℹ️ Type-only import: 4 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export 5 | import type { AppRouter } from 'server/trpc/routers/index'; 6 | import superjson from 'superjson'; 7 | 8 | function getBaseUrl() { 9 | // browser should use relative url 10 | if (typeof window !== 'undefined') return ''; 11 | 12 | // reference for vercel.com SSR 13 | if (process.env.VERCEL_URL) { 14 | return `https://${process.env.VERCEL_URL}`; 15 | } 16 | 17 | // reference for render.com SSR 18 | if (process.env.RENDER_INTERNAL_HOSTNAME) { 19 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 20 | } 21 | 22 | // override for docker etc SSR 23 | if (process.env.NEXT_PUBLIC_APP_URL) return process.env.NEXT_PUBLIC_APP_URL; 24 | 25 | // assume localhost in dev SSR 26 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 27 | } 28 | 29 | export const trpc = createTRPCNext({ 30 | /** 31 | * Config options: 32 | * https://trpc.io/docs/v10/nextjs#createtrpcnext-options 33 | */ 34 | config() { 35 | return { 36 | /** 37 | * @link https://trpc.io/docs/v10/data-transformers 38 | */ 39 | transformer: superjson, 40 | /** 41 | * @link https://trpc.io/docs/v10/links 42 | */ 43 | links: [ 44 | // adds pretty logs to your console in development and logs errors in production 45 | loggerLink({ 46 | enabled: () => true, 47 | }), 48 | httpBatchLink({ 49 | url: `${getBaseUrl()}/api/trpc`, 50 | }), 51 | ], 52 | }; 53 | }, 54 | /** 55 | * @link https://trpc.io/docs/v10/ssr 56 | */ 57 | ssr: false, 58 | }); 59 | 60 | /** 61 | * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`. 62 | * @link https://trpc.io/docs/v10/react#2-create-trpc-hooks 63 | */ 64 | 65 | /** 66 | * You can use inference to get types for procedure input and output 67 | * this is equivalent to inferQueryOutput/inferQueryInput/inferMutationOutput/inferMutationInput in v9 68 | * @example type SourceInput = inferProcedureInput; 69 | * @example type SourceOutput = inferProcedureOutput; 70 | */ 71 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main, 0.x ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '27 0 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'typescript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /src/feature/react-hook-form/Form.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from '@hookform/resolvers/zod'; 2 | import { useId } from 'react'; 3 | import { 4 | FieldValues, 5 | FormProvider, 6 | SubmitHandler, 7 | useForm, 8 | useFormContext, 9 | UseFormProps, 10 | UseFormReturn, 11 | } from 'react-hook-form'; 12 | import { z } from 'zod'; 13 | 14 | type UseZodForm = UseFormReturn & { 15 | /** 16 | * A unique ID for this form. 17 | */ 18 | id: string; 19 | }; 20 | export function useZodForm( 21 | props: Omit, 'resolver'> & { 22 | schema: TSchema; 23 | }, 24 | ) { 25 | const form = useForm({ 26 | ...props, 27 | resolver: zodResolver(props.schema, undefined, { 28 | // This makes it so we can use `.transform()`s on the schema without same transform getting applied again when it reaches the server 29 | raw: true, 30 | }), 31 | }) as UseZodForm; 32 | 33 | form.id = useId(); 34 | 35 | return form; 36 | } 37 | 38 | type AnyZodForm = UseZodForm; 39 | 40 | export function Form( 41 | props: Omit, 'onSubmit' | 'id'> & { 42 | handleSubmit: SubmitHandler; 43 | form: UseZodForm; 44 | }, 45 | ) { 46 | const { handleSubmit, form, ...passThrough }: typeof props = props; 47 | return ( 48 | 49 |
{ 53 | form.handleSubmit(async (values) => { 54 | try { 55 | await handleSubmit(values); 56 | } catch (cause) { 57 | form.setError('root.server', { 58 | message: (cause as Error)?.message ?? 'Unknown error', 59 | type: 'server', 60 | }); 61 | } 62 | })(event); 63 | }} 64 | /> 65 | 66 | ); 67 | } 68 | 69 | export function SubmitButton( 70 | props: Omit, 'type' | 'form'> & { 71 | /** 72 | * Optionally specify a form to submit instead of the closest form context. 73 | */ 74 | form?: AnyZodForm; 75 | }, 76 | ) { 77 | const context = useFormContext(); 78 | 79 | const form = props.form ?? context; 80 | if (!form) { 81 | throw new Error( 82 | 'SubmitButton must be used within a Form or have a form prop', 83 | ); 84 | } 85 | const { formState } = form; 86 | 87 | return ( 88 | 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/feature/react-hook-form/index.tsx: -------------------------------------------------------------------------------- 1 | import { trpc } from 'utils/trpc'; 2 | import { z } from 'zod'; 3 | 4 | import { Form, SubmitButton, useZodForm } from './Form'; 5 | 6 | // validation schema is used by tRPC mutation and client 7 | export const validationSchema = z.object({ 8 | title: z.string().min(2), 9 | text: z.string().min(5), 10 | }); 11 | 12 | function AddPostForm() { 13 | const utils = trpc.useContext().reactHookFormRouter; 14 | 15 | const mutation = trpc.reactHookFormRouter.add.useMutation({ 16 | onSuccess: async () => { 17 | await utils.list.invalidate(); 18 | }, 19 | }); 20 | 21 | const form = useZodForm({ 22 | schema: validationSchema, 23 | defaultValues: { 24 | title: '', 25 | text: '', 26 | }, 27 | }); 28 | 29 | return ( 30 | <> 31 | { 34 | await mutation.mutateAsync(values); 35 | form.reset(); 36 | }} 37 | className="space-y-2" 38 | > 39 |
40 | 45 | 46 | {form.formState.errors.title?.message && ( 47 |

48 | {form.formState.errors.title?.message} 49 |

50 | )} 51 |
52 |
53 |