├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .watchmanconfig ├── docs ├── .npmrc ├── tsconfig.build.json ├── public │ ├── logo.png │ ├── banner.png │ ├── favicon.ico │ ├── preview.png │ ├── docs-logo.png │ ├── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── site.webmanifest │ ├── robots.txt │ ├── sitemap.xml │ ├── icons │ │ ├── npm.svg │ │ ├── pnpm.svg │ │ ├── yarn.svg │ │ ├── typescript.svg │ │ └── trpc.svg │ └── sitemap-0.xml ├── postcss.config.js ├── .eslintrc.json ├── pages │ ├── index.mdx │ ├── _app.mdx │ ├── _meta.json │ └── docs │ │ ├── _meta.json │ │ ├── dependency-injection.mdx │ │ ├── nestjs.mdx │ │ ├── graphql.mdx │ │ ├── structure.mdx │ │ ├── trpc.mdx │ │ ├── index.mdx │ │ └── integrations.mdx ├── types │ └── index.ts ├── next-sitemap.config.js ├── next-env.d.ts ├── svgr.d.ts ├── utils │ └── searchParams.ts ├── .gitignore ├── components │ ├── Iframe │ │ └── index.tsx │ ├── FeatureCard │ │ └── index.tsx │ ├── Footer │ │ └── index.tsx │ ├── Table │ │ └── index.tsx │ ├── Preview │ │ └── index.tsx │ └── Home │ │ └── index.tsx ├── tailwind.config.js ├── tsconfig.json ├── package.json ├── next.config.js ├── styles │ └── globals.css └── theme.config.jsx ├── .yarnrc.yml ├── .prettierrc ├── .prettierignore ├── packages ├── nestjs-trpc │ ├── lib │ │ ├── drivers │ │ │ ├── index.ts │ │ │ ├── express.driver.ts │ │ │ └── fastify.driver.ts │ │ ├── trpc.enum.ts │ │ ├── index.ts │ │ ├── generators │ │ │ ├── generator.constants.ts │ │ │ ├── generator.interface.ts │ │ │ ├── __tests__ │ │ │ │ ├── context.generator.spec.ts │ │ │ │ ├── middleware.generator.spec.ts │ │ │ │ ├── procedure.generator.spec.ts │ │ │ │ └── decorator.generator.spec.ts │ │ │ ├── static.generator.ts │ │ │ ├── context.generator.ts │ │ │ ├── decorator.generator.ts │ │ │ ├── router.generator.ts │ │ │ ├── generator.module.ts │ │ │ └── middleware.generator.ts │ │ ├── interfaces │ │ │ ├── scanner.interface.ts │ │ │ ├── index.ts │ │ │ ├── procedure-options.interface.ts │ │ │ ├── context.interface.ts │ │ │ ├── middleware.interface.ts │ │ │ ├── generator.interface.ts │ │ │ ├── module-options.interface.ts │ │ │ └── factory.interface.ts │ │ ├── utils │ │ │ ├── ts-morph.util.ts │ │ │ └── validate-each.util.ts │ │ ├── scanners │ │ │ ├── scanner.module.ts │ │ │ ├── file.scanner.ts │ │ │ └── imports.scanner.ts │ │ ├── decorators │ │ │ ├── index.ts │ │ │ ├── context.decorator.ts │ │ │ ├── query.decorator.ts │ │ │ ├── mutation.decorator.ts │ │ │ ├── router.decorator.ts │ │ │ ├── path.decorator.ts │ │ │ ├── type.decorator.ts │ │ │ ├── options.decorator.ts │ │ │ ├── raw-input.decorator.ts │ │ │ ├── input.decorator.ts │ │ │ ├── __tests__ │ │ │ │ ├── middlewares.decorator.spec.ts │ │ │ │ ├── router.decorator.spec.ts │ │ │ │ ├── type.decorator.spec.ts │ │ │ │ ├── path.decorator.spec.ts │ │ │ │ ├── options.decorator.spec.ts │ │ │ │ ├── raw-input.decorator.spec.ts │ │ │ │ ├── input.decorator.spec.ts │ │ │ │ ├── context.decorator.spec.ts │ │ │ │ ├── query.decorator.spec.ts │ │ │ │ └── mutation.decorator.spec.ts │ │ │ └── middlewares.decorator.ts │ │ ├── trpc.constants.ts │ │ ├── app-router.host.ts │ │ ├── factories │ │ │ ├── factory.module.ts │ │ │ ├── trpc.factory.ts │ │ │ ├── __tests__ │ │ │ │ ├── trpc.factory.spec.ts │ │ │ │ └── middleware.factory.spec.ts │ │ │ ├── middleware.factory.ts │ │ │ └── router.factory.ts │ │ ├── trpc.module.ts │ │ └── trpc.driver.ts │ ├── .npmignore │ ├── tsconfig.spec.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── .gitignore │ ├── jest.config.js │ ├── package.json │ └── README.md ├── tsconfig.json └── tsconfig.build.json ├── examples ├── nextjs-trpc │ ├── next.config.js │ ├── postcss.config.js │ ├── app │ │ ├── trpc.ts │ │ ├── page.tsx │ │ ├── client-side.tsx │ │ ├── layout.tsx │ │ └── globals.css │ ├── .gitignore │ ├── tailwind.config.js │ ├── package.json │ └── tsconfig.json ├── tsconfig.json ├── nestjs-express │ ├── src │ │ ├── user.schema.ts │ │ ├── main.ts │ │ ├── user.controller.ts │ │ ├── user.service.ts │ │ ├── app.context.ts │ │ ├── trpc-panel.controller.ts │ │ ├── app.module.ts │ │ ├── protected.middleware.ts │ │ └── user.router.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── .gitignore │ ├── nest-cli.json │ └── package.json ├── nestjs-fastify │ ├── src │ │ ├── user.schema.ts │ │ ├── user.service.ts │ │ ├── user.controller.ts │ │ ├── main.ts │ │ ├── app.context.ts │ │ ├── protected.middleware.ts │ │ ├── logging.middleware.ts │ │ ├── app.module.ts │ │ ├── trpc-panel.controller.ts │ │ └── user.router.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── .gitignore │ ├── nest-cli.json │ └── package.json └── tsconfig.build.json ├── renovate.json ├── .npmignore ├── .gitignore ├── .commitlintrc.json ├── .github └── workflows │ └── verify.yml ├── .release-it.json ├── LICENSE ├── eslint.config.mjs ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | package-manager-strict=false -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint -c --config .commitlintrc.json -E --edit -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: pnp 2 | pnpMode: strict 3 | enableTelemetry: false -------------------------------------------------------------------------------- /docs/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/banner.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/preview.png -------------------------------------------------------------------------------- /docs/public/docs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/docs-logo.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | docs/ 4 | public/ 5 | *.md 6 | *.mdx 7 | packages/**/__tests__/*.ts -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/drivers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fastify.driver'; 2 | export * from './express.driver'; 3 | -------------------------------------------------------------------------------- /docs/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/trpc.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ProcedureType { 2 | Query = 'Query', 3 | Mutation = 'Mutation', 4 | } 5 | -------------------------------------------------------------------------------- /docs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "prettier/prettier": "warn" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /examples/nextjs-trpc/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig -------------------------------------------------------------------------------- /examples/nextjs-trpc/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /docs/pages/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC: Bringing type-safety to NestJS" 3 | --- 4 | 5 | import Home from "../components/Home" 6 | 7 | -------------------------------------------------------------------------------- /docs/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinEdry/nestjs-trpc/HEAD/docs/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/types/index.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export type IconSvgProps = SVGProps & { 4 | size?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./nestjs-trpc/tsconfig.build.json" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./nestjs-express/tsconfig.build.json" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: 'https://nestjs-trpc.io', 4 | generateRobotsTxt: true, 5 | }; -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trpc.module'; 2 | export * from './interfaces'; 3 | export * from './decorators'; 4 | export * from './app-router.host'; 5 | -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # Host 6 | Host: https://nestjs-trpc.io 7 | 8 | # Sitemaps 9 | Sitemap: https://nestjs-trpc.io/sitemap.xml 10 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/generator.constants.ts: -------------------------------------------------------------------------------- 1 | export const TYPESCRIPT_PROJECT = 'TypescriptProject'; 2 | export const TYPESCRIPT_APP_ROUTER_SOURCE_FILE = 3 | 'TypescriptAppRouterSourceFile'; 4 | -------------------------------------------------------------------------------- /docs/public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://nestjs-trpc.io/sitemap-0.xml 4 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/scanner.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SourceMapping { 2 | version: number; 3 | file: string; 4 | sourceRoot: string; 5 | sources: Array; 6 | mappings: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module-options.interface'; 2 | export * from './middleware.interface'; 3 | export * from './procedure-options.interface'; 4 | export * from './context.interface'; 5 | -------------------------------------------------------------------------------- /docs/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 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "labels": ["dependencies"], 4 | "packageRules": [ 5 | { 6 | "depTypeList": ["devDependencies"], 7 | "automerge": true 8 | } 9 | ], 10 | "extends": ["config:base"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/nestjs-express/src/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const userSchema = z.object({ 4 | name: z.string(), 5 | email: z.string(), 6 | password: z.string(), 7 | }); 8 | 9 | export type User = z.infer; 10 | -------------------------------------------------------------------------------- /examples/nestjs-express/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src" 6 | }, 7 | "exclude": ["node_modules", "dist", "src/**/__tests__/*", "*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const userSchema = z.object({ 4 | name: z.string(), 5 | email: z.string(), 6 | password: z.string(), 7 | }); 8 | 9 | export type User = z.infer; 10 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src" 6 | }, 7 | "exclude": ["node_modules", "dist", "src/**/__tests__/*", "*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | tests 4 | index.ts 5 | tsconfig.json 6 | .prettierrc 7 | 8 | # misc 9 | .commitlintrc.json 10 | .release-it.json 11 | .eslintignore 12 | .eslintrc.js 13 | renovate.json 14 | .prettierignore 15 | .prettierrc -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | tests 4 | index.ts 5 | package-lock.json 6 | tsconfig.json 7 | .prettierrc 8 | 9 | # misc 10 | .commitlintrc.json 11 | .release-it.json 12 | .eslintignore 13 | .eslintrc.js 14 | renovate.json 15 | .prettierignore 16 | .prettierrc -------------------------------------------------------------------------------- /docs/svgr.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import { FC, SVGProps } from 'react' 3 | const content: FC> 4 | export default content 5 | } 6 | 7 | declare module '*.svg?url' { 8 | const content: any 9 | export default content 10 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/utils/ts-morph.util.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from 'ts-morph'; 2 | 3 | export async function saveOrOverrideFile( 4 | sourceFile: SourceFile, 5 | ): Promise { 6 | sourceFile.formatText({ indentSize: 2 }); 7 | await sourceFile.save(); 8 | } 9 | -------------------------------------------------------------------------------- /docs/public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /examples/nestjs-express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.build.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.build.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "sourceMap": true, 6 | "outDir": "./dist/tests", 7 | "types": ["jest", "node"], 8 | "emitDecoratorMetadata": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/nestjs-express/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule, { cors: true }); 6 | 7 | await app.listen(8080); 8 | } 9 | 10 | void bootstrap(); 11 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/procedure-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ProcedureParams } from '@trpc/server'; 2 | import { ResolveOptions } from '@trpc/server/dist/core/internals/utils'; 3 | 4 | export type ProcedureOptions = ResolveOptions & { 5 | type: string; 6 | path: string; 7 | rawInput: string; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/nextjs-trpc/app/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; 2 | import { AppRouter } from "@server/@generated/server"; 3 | 4 | export const trpc = createTRPCProxyClient({ 5 | links: [ 6 | httpBatchLink({ 7 | url: `${process.env.NEXT_PUBLIC_NESTJS_SERVER}/trpc`, 8 | }), 9 | ], 10 | }); -------------------------------------------------------------------------------- /examples/nestjs-express/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.vscode 7 | 8 | # misc 9 | npm-debug.log 10 | .DS_Store 11 | 12 | # examples 13 | /test 14 | /coverage 15 | /.nyc_output 16 | test-schema.graphql 17 | *.test-definitions.ts 18 | 19 | # dist 20 | dist 21 | **/*.tsbuildinfo 22 | 23 | .yarn/ 24 | 25 | src/@generated -------------------------------------------------------------------------------- /examples/nestjs-fastify/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.vscode 7 | 8 | # misc 9 | npm-debug.log 10 | .DS_Store 11 | 12 | # examples 13 | /test 14 | /coverage 15 | /.nyc_output 16 | test-schema.graphql 17 | *.test-definitions.ts 18 | 19 | # dist 20 | dist 21 | **/*.tsbuildinfo 22 | 23 | .yarn/ 24 | 25 | src/@generated -------------------------------------------------------------------------------- /packages/nestjs-trpc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./lib", 6 | "incremental": false, 7 | "experimentalDecorators": true 8 | }, 9 | "include": ["lib/**/*.ts"], 10 | "exclude": ["node_modules", "dist", "lib/**/__tests__/*", "*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /docs/pages/_app.mdx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import { Analytics } from "@vercel/analytics/react"; 3 | import { GeistSans } from 'geist/font/sans' 4 | 5 | export default function App({ Component, pageProps }) { 6 | return ( 7 | 8 | 9 | 10 | 11 | ) 12 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/generator.interface.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaImports, TRPCContext } from '../interfaces'; 2 | import type { Class } from 'type-fest'; 3 | 4 | export interface GeneratorModuleOptions { 5 | rootModuleFilePath: string; 6 | context?: Class; 7 | outputDirPath?: string; 8 | schemaFileImports?: Array; 9 | } 10 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/scanners/scanner.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FileScanner } from './file.scanner'; 3 | import { ImportsScanner } from './imports.scanner'; 4 | 5 | @Module({ 6 | imports: [], 7 | providers: [FileScanner, ImportsScanner], 8 | exports: [FileScanner, ImportsScanner], 9 | }) 10 | export class ScannerModule {} 11 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "experimentalDecorators": true 6 | }, 7 | "files": [], 8 | "include": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.build.json" 12 | }, 13 | { 14 | "path": "./tsconfig.spec.json" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/nestjs-express/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | }, 8 | "watchOptions": { 9 | "watchExclude": [ 10 | "src/@generated/**/*", 11 | "src/@generated/*", 12 | "**/@generated/**/*", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | }, 8 | "watchOptions": { 9 | "watchExclude": [ 10 | "src/@generated/**/*", 11 | "src/@generated/*", 12 | "**/@generated/**/*", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs-trpc/app/page.tsx: -------------------------------------------------------------------------------- 1 | import ClientSide from "./client-side"; 2 | import { trpc } from "./trpc"; 3 | 4 | export default async function Home() { 5 | const userId = "randomUserId" 6 | const response = await trpc.users.getUserById.query({ userId }); 7 | 8 | return ( 9 | 10 | Server side - {response.name} 11 | 12 | 13 | ); 14 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # generated types 5 | /types 6 | 7 | # IDE 8 | /.idea 9 | /.awcache 10 | /.vscode 11 | 12 | # misc 13 | npm-debug.log 14 | .DS_Store 15 | 16 | # examples 17 | /test 18 | /coverage 19 | /.nyc_output 20 | test-schema.graphql 21 | *.test-definitions.ts 22 | 23 | # dist 24 | dist 25 | test/**/dist 26 | **/*.tsbuildinfo 27 | 28 | .yarn/ -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { User } from './user.schema'; 3 | 4 | @Injectable() 5 | export class UserService { 6 | async test(): Promise { 7 | return 'test'; 8 | } 9 | 10 | async getUser(userId: string): Promise { 11 | return { 12 | name: 'user', 13 | email: 'user@email.com', 14 | password: '0000', 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/nestjs-express/src/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common'; 2 | 3 | @Controller('cats') 4 | export class CatsController { 5 | @Get() 6 | findAll(): string { 7 | return 'This action returns all cats'; 8 | } 9 | 10 | @Post() 11 | findPAll(): string { 12 | return 'This action returns all cats'; 13 | } 14 | 15 | @Post('bla') 16 | returnBla() { 17 | return 'This action returns all cats'; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common'; 2 | 3 | @Controller('cats') 4 | export class CatsController { 5 | @Get() 6 | findAll(): string { 7 | return 'This action returns all cats'; 8 | } 9 | 10 | @Post() 11 | findPAll(): string { 12 | return 'This action returns all cats'; 13 | } 14 | 15 | @Post('bla') 16 | returnBla() { 17 | return 'This action returns all cats'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from '@nestjs/platform-fastify'; 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create( 10 | AppModule, 11 | new FastifyAdapter(), 12 | { cors: true }, 13 | ); 14 | 15 | await app.listen(8080); 16 | } 17 | 18 | void bootstrap(); 19 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/context.interface.ts: -------------------------------------------------------------------------------- 1 | import type { CreateExpressContextOptions } from '@trpc/server/adapters/express'; 2 | import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; 3 | 4 | export type ContextOptions = 5 | | CreateExpressContextOptions 6 | | CreateFastifyContextOptions; 7 | 8 | export interface TRPCContext { 9 | create( 10 | opts: ContextOptions, 11 | ): Record | Promise>; 12 | } 13 | -------------------------------------------------------------------------------- /docs/utils/searchParams.ts: -------------------------------------------------------------------------------- 1 | const isArray = (value: unknown): value is Readonly | unknown[] => 2 | Array.isArray(value); 3 | 4 | export function searchParams( 5 | obj: Record | string[] | string>, 6 | ): string { 7 | return Object.entries(obj) 8 | .map(([key, value]) => { 9 | const values = isArray(value) ? value : [value]; 10 | 11 | return values.map((v) => `${key}=${encodeURIComponent(v)}`).join('&'); 12 | }) 13 | .join('&'); 14 | } -------------------------------------------------------------------------------- /examples/nextjs-trpc/app/client-side.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { trpc } from "./trpc"; 5 | 6 | export default function ClientSide() { 7 | const [greeting, setGreeting] = useState(""); 8 | 9 | useEffect(() => { 10 | const userId = "randomUserId" 11 | trpc.users.getUserById.query({userId}).then((response) => { 12 | setGreeting(response.name); 13 | }); 14 | }); 15 | 16 | return I am client side - {greeting}; 17 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/lib'], 5 | testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], 6 | transform: { 7 | '^.+\\.tsx?$': ['ts-jest', { 8 | tsconfig: 'tsconfig.spec.json', 9 | diagnostics: { 10 | ignoreCodes: [151001] 11 | } 12 | }] 13 | }, 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 15 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | .pnp.* 5 | 6 | # generated types 7 | /types 8 | 9 | # IDE 10 | /.idea 11 | /.awcache 12 | /.vscode 13 | 14 | # misc 15 | npm-debug.log 16 | .DS_Store 17 | 18 | # examples 19 | /test 20 | /coverage 21 | /.nyc_output 22 | test-schema.graphql 23 | *.test-definitions.ts 24 | 25 | # dist 26 | dist 27 | test/**/dist 28 | **/*.tsbuildinfo 29 | 30 | .yarn/* 31 | !.yarn/cache 32 | !.yarn/patches 33 | !.yarn/plugins 34 | !.yarn/releases 35 | !.yarn/sdks 36 | !.yarn/versions -------------------------------------------------------------------------------- /examples/nestjs-express/src/user.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Inject, 4 | Injectable, 5 | forwardRef, 6 | } from '@nestjs/common'; 7 | import { User } from './user.schema'; 8 | 9 | @Injectable() 10 | export class UserService { 11 | async test(): Promise { 12 | return 'test'; 13 | } 14 | 15 | async getUser(userId: string): Promise { 16 | return { 17 | name: 'user', 18 | email: 'user@email.com', 19 | password: '0000', 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './router.decorator'; 2 | 3 | // Procedure Decorators 4 | export * from './middlewares.decorator'; 5 | export * from './mutation.decorator'; 6 | export * from './query.decorator'; 7 | 8 | // Procedure Param Decorators 9 | export * from './options.decorator'; 10 | export * from './context.decorator'; 11 | export * from './input.decorator'; 12 | export * from './raw-input.decorator'; 13 | export * from './type.decorator'; 14 | export * from './path.decorator'; 15 | -------------------------------------------------------------------------------- /examples/nextjs-trpc/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Inter } from 'next/font/google' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Create Next App', 8 | description: 'Generated by create next app', 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /examples/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": false, 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "noImplicitAny": false, 8 | "incremental": false, 9 | "importHelpers": true, 10 | "removeComments": false, 11 | "noLib": false, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "ES2021", 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/nextjs-trpc/.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts -------------------------------------------------------------------------------- /docs/public/icons/npm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/nextjs-trpc/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } -------------------------------------------------------------------------------- /docs/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "title": "Introduction", 4 | "display": "hidden", 5 | "theme": { 6 | "breadcrumb": false, 7 | "footer": true, 8 | "sidebar": false, 9 | "toc": false, 10 | "pagination": false, 11 | "timestamp": false, 12 | "layout": "full" 13 | } 14 | }, 15 | "docs": { 16 | "title": "Docs", 17 | "type": "page" 18 | }, 19 | "contact": { 20 | "title": "Contact ↗", 21 | "type": "page", 22 | "href": "https://twitter.com/KevinEdry", 23 | "newWindow": true 24 | } 25 | } -------------------------------------------------------------------------------- /docs/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | ../website/nestjs-trpc.io/next-env.d.ts 37 | -------------------------------------------------------------------------------- /docs/components/Iframe/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import type { ComponentPropsWithoutRef } from 'react'; 3 | import React, { useState } from 'react'; 4 | 5 | export const Iframe = ( 6 | props: { 7 | setLoaded: (loaded: boolean) => void; 8 | } & Omit, 'className'>, 9 | ) => { 10 | const { setLoaded } = props; 11 | return ( 12 | { 16 | setLoaded(true); 17 | }} 18 | className={'h-full w-full rounded-xl'} 19 | /> 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /packages/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "incremental": false, 6 | "declarationMap": true, 7 | "noImplicitAny": false, 8 | "importHelpers": true, 9 | "esModuleInterop": true, 10 | "removeComments": false, 11 | "noLib": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "target": "ES2021", 16 | "sourceMap": true, 17 | "skipLibCheck": true, 18 | "resolveJsonModule": true, 19 | "strict": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/nestjs-express/src/app.context.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ContextOptions, TRPCContext } from 'nestjs-trpc'; 3 | import { UserService } from './user.service'; 4 | 5 | @Injectable() 6 | export class AppContext implements TRPCContext { 7 | constructor(@Inject(UserService) private readonly userService: UserService) {} 8 | 9 | async create(opts: ContextOptions): Promise> { 10 | const res = await this.userService.test(); 11 | return { 12 | req: opts.req, 13 | auth: { 14 | user: res, 15 | }, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/app.context.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ContextOptions, TRPCContext } from 'nestjs-trpc'; 3 | import { UserService } from './user.service'; 4 | 5 | @Injectable() 6 | export class AppContext implements TRPCContext { 7 | constructor(@Inject(UserService) private readonly userService: UserService) {} 8 | 9 | async create(opts: ContextOptions): Promise> { 10 | const res = await this.userService.test(); 11 | return { 12 | req: opts.req, 13 | auth: { 14 | user: res, 15 | }, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/protected.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MiddlewareOptions, 3 | MiddlewareResponse, 4 | TRPCMiddleware, 5 | } from 'nestjs-trpc'; 6 | import { Inject, Injectable } from '@nestjs/common'; 7 | import { UserService } from './user.service'; 8 | 9 | @Injectable() 10 | export class ProtectedMiddleware implements TRPCMiddleware { 11 | constructor(@Inject(UserService) private readonly userService: UserService) {} 12 | async use(opts: MiddlewareOptions): Promise { 13 | return opts.next({ 14 | ctx: { 15 | ben: 1, 16 | }, 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/trpc.constants.ts: -------------------------------------------------------------------------------- 1 | export const TRPC_MODULE_OPTIONS = 'TrpcModuleOptions'; 2 | 3 | export const TRPC_GENERATOR_OPTIONS = 'TrpcGenderOptions'; 4 | export const TRPC_MODULE_CALLER_FILE_PATH = 'TrpcModuleCallerFilePath'; 5 | 6 | export const ROUTER_METADATA_KEY = Symbol('trpc:router_type'); 7 | 8 | export const PROCEDURE_TYPE_KEY = Symbol('trpc:procedure_type'); 9 | export const PROCEDURE_METADATA_KEY = Symbol('trpc:procedure_metadata'); 10 | 11 | export const PROCEDURE_PARAM_METADATA_KEY = Symbol( 12 | 'trpc:procedure_param_metadata', 13 | ); 14 | 15 | export const MIDDLEWARES_KEY = Symbol('trpc:middlewares_key'); 16 | 17 | // Logging Constants 18 | export const LOGGER_CONTEXT = 'TRPC'; 19 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx,md,mdx}", 5 | "./pages/**/*.{js,ts,jsx,tsx,md,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,md,mdx}", 7 | // Or if using `src` directory: 8 | "./src/**/*.{js,ts,jsx,tsx,md,mdx}", 9 | ], 10 | theme: { 11 | colors: { 12 | primary: "#398CCB", 13 | subtext: "#8BA1B2", 14 | white: "#FFF", 15 | black: "#000", 16 | gray: "#D9D9D9", 17 | "background-black": "#070707", 18 | "border-gray": "#788188", 19 | "border-primary": "#75ABD4" 20 | }, 21 | extend: {}, 22 | }, 23 | darkMode: "class", 24 | plugins: [], 25 | } -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/logging.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MiddlewareOptions, 3 | MiddlewareResponse, 4 | TRPCMiddleware, 5 | } from 'nestjs-trpc'; 6 | import { Injectable } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class LoggingMiddleware implements TRPCMiddleware { 10 | async use(opts: MiddlewareOptions): Promise { 11 | const start = Date.now(); 12 | const result = await opts.next(); 13 | const durationMs = Date.now() - start; 14 | const meta = { path: opts.path, type: opts.type, durationMs }; 15 | result.ok 16 | ? console.log('OK request timing:', meta) 17 | : console.error('Non-OK request timing', meta); 18 | return result; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/nextjs-trpc/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } -------------------------------------------------------------------------------- /examples/nextjs-trpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 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 | }, 11 | "dependencies": { 12 | "@trpc/client": "^10.33.0", 13 | "@trpc/server": "^10.33.0", 14 | "next": "13.4.7", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "18.11.18", 20 | "@types/react": "18.2.14", 21 | "@types/react-dom": "18.2.6", 22 | "autoprefixer": "10.4.14", 23 | "postcss": "8.4.24", 24 | "tailwindcss": "3.3.2", 25 | "typescript": "^4.7.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: verify 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, reopened] 9 | 10 | jobs: 11 | verify: 12 | runs-on: ubuntu-latest 13 | name: verify 14 | steps: 15 | - name: Checkout the repo 16 | uses: actions/checkout@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | submodules: false 20 | - name: Enable Corepack 21 | run: corepack enable 22 | - name: Install dependencies 23 | run: yarn install --frozen-lockfile 24 | - name: Lint 25 | run: yarn lint --no-fix 26 | - name: Test 27 | run: yarn test 28 | - name: Build 29 | run: yarn build 30 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/app-router.host.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AnyRouter } from '@trpc/server'; 3 | 4 | @Injectable() 5 | export class AppRouterHost { 6 | private _appRouter: AnyRouter | undefined; 7 | 8 | set appRouter(schemaRef: AnyRouter) { 9 | this._appRouter = schemaRef; 10 | } 11 | 12 | get appRouter(): AnyRouter { 13 | if (!this._appRouter) { 14 | throw new Error( 15 | 'TRPC appRouter has not yet been created. ' + 16 | 'Make sure to call the "AppRouterHost#appRouter" getter when the application is already initialized (after the "onModuleInit" hook triggered by either "app.listen()" or "app.init()" method).', 17 | ); 18 | } 19 | return this._appRouter; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/components/FeatureCard/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export default function FeatureCard({ Icon, title, description }) { 4 | return ( 5 | 9 | 10 | 11 | {title} 12 | {description} 13 | 14 | 15 | )} -------------------------------------------------------------------------------- /examples/nestjs-express/src/trpc-panel.controller.ts: -------------------------------------------------------------------------------- 1 | import { All, Controller, Inject, OnModuleInit } from '@nestjs/common'; 2 | import { renderTrpcPanel } from 'trpc-panel'; 3 | import { AnyRouter } from '@trpc/server'; 4 | import { AppRouterHost } from 'nestjs-trpc'; 5 | 6 | @Controller() 7 | export class TrpcPanelController implements OnModuleInit { 8 | private appRouter!: AnyRouter; 9 | 10 | constructor( 11 | @Inject(AppRouterHost) private readonly appRouterHost: AppRouterHost, 12 | ) {} 13 | 14 | onModuleInit() { 15 | this.appRouter = this.appRouterHost.appRouter; 16 | } 17 | 18 | @All('/panel') 19 | panel(): string { 20 | return renderTrpcPanel(this.appRouter, { 21 | url: 'http://localhost:8080/trpc', 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/factories/factory.module.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger, Module } from '@nestjs/common'; 2 | import { MetadataScanner } from '@nestjs/core'; 3 | import { TRPCFactory } from './trpc.factory'; 4 | import { RouterFactory } from './router.factory'; 5 | import { ProcedureFactory } from './procedure.factory'; 6 | import { MiddlewareFactory } from './middleware.factory'; 7 | 8 | @Module({ 9 | imports: [], 10 | providers: [ 11 | // NestJS Providers 12 | ConsoleLogger, 13 | MetadataScanner, 14 | 15 | // Local Providers 16 | TRPCFactory, 17 | RouterFactory, 18 | ProcedureFactory, 19 | MiddlewareFactory, 20 | ], 21 | exports: [TRPCFactory, RouterFactory, ProcedureFactory, MiddlewareFactory], 22 | }) 23 | export class FactoryModule {} 24 | -------------------------------------------------------------------------------- /examples/nestjs-express/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserRouter } from './user.router'; 3 | import { TRPCModule } from 'nestjs-trpc'; 4 | import { UserService } from './user.service'; 5 | import { ProtectedMiddleware } from './protected.middleware'; 6 | import { AppContext } from './app.context'; 7 | import { TrpcPanelController } from './trpc-panel.controller'; 8 | 9 | @Module({ 10 | imports: [ 11 | TRPCModule.forRoot({ 12 | autoSchemaFile: 13 | process.env.NODE_ENV === 'production' ? undefined : './src/@generated', 14 | context: AppContext, 15 | }), 16 | ], 17 | controllers: [TrpcPanelController], 18 | providers: [UserRouter, AppContext, UserService, ProtectedMiddleware], 19 | }) 20 | export class AppModule {} 21 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserRouter } from './user.router'; 3 | import { TRPCModule } from 'nestjs-trpc'; 4 | import { UserService } from './user.service'; 5 | import { ProtectedMiddleware } from './protected.middleware'; 6 | import { AppContext } from './app.context'; 7 | import { TrpcPanelController } from './trpc-panel.controller'; 8 | import { LoggingMiddleware } from './logging.middleware'; 9 | 10 | @Module({ 11 | imports: [ 12 | TRPCModule.forRoot({ 13 | autoSchemaFile: './src/@generated', 14 | context: AppContext, 15 | }), 16 | ], 17 | controllers: [TrpcPanelController], 18 | providers: [ 19 | UserRouter, 20 | AppContext, 21 | UserService, 22 | ProtectedMiddleware, 23 | LoggingMiddleware, 24 | ], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/factories/trpc.factory.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { MergeRouters } from '@trpc/server/dist/core/internals/mergeRouters'; 3 | import { AnyRouterDef } from '@trpc/server/dist/core/router'; 4 | import { RouterFactory } from './router.factory'; 5 | import { TRPCRouter } from '../interfaces/factory.interface'; 6 | import { AnyRouter, ProcedureBuilder } from '@trpc/server'; 7 | 8 | @Injectable() 9 | export class TRPCFactory { 10 | @Inject(RouterFactory) 11 | private readonly routerFactory!: RouterFactory; 12 | 13 | serializeAppRoutes( 14 | router: TRPCRouter, 15 | procedure: ProcedureBuilder, 16 | ): MergeRouters, AnyRouterDef> { 17 | const routerSchema = this.routerFactory.serializeRoutes(router, procedure); 18 | return router(routerSchema); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "outDir": "./next", 20 | "rootDir": "./", 21 | "baseUrl": "./", 22 | "jsx": "preserve", 23 | "incremental": true 24 | }, 25 | "paths": { 26 | "@/*": [ 27 | "./*" 28 | ] 29 | }, 30 | "include": [ 31 | "next-env.d.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | "svgr.d.ts" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/middleware.interface.ts: -------------------------------------------------------------------------------- 1 | import type { ProcedureType, ProcedureParams } from '@trpc/server'; 2 | import type { MiddlewareResult } from '@trpc/server/dist/core/middleware'; 3 | 4 | export type MiddlewareResponse = 5 | | Promise> 6 | | (<$Context>(opts: { 7 | ctx: $Context; 8 | }) => Promise>); 9 | 10 | export type MiddlewareOptions = { 11 | ctx: TContext; 12 | type: ProcedureType; 13 | path: string; 14 | input: unknown; 15 | rawInput: unknown; 16 | meta: unknown; 17 | next: (opts?: { 18 | ctx: Record; 19 | }) => Promise>; 20 | }; 21 | 22 | export interface TRPCMiddleware { 23 | use( 24 | opts: MiddlewareOptions, 25 | ): MiddlewareResponse | Promise; 26 | } 27 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/utils/validate-each.util.ts: -------------------------------------------------------------------------------- 1 | export class InvalidDecoratorItemException extends Error { 2 | private readonly msg: string; 3 | 4 | constructor(decorator: string, item: string, context: string) { 5 | const message = `Invalid ${item} passed to ${decorator}() decorator (${context}).`; 6 | super(message); 7 | 8 | this.msg = message; 9 | } 10 | 11 | public what(): string { 12 | return this.msg; 13 | } 14 | } 15 | 16 | export function validateEach( 17 | context: { name: string }, 18 | arr: any[], 19 | predicate: (...args: any) => unknown, 20 | decorator: string, 21 | item: string, 22 | ): boolean { 23 | if (!context || !context.name) { 24 | return true; 25 | } 26 | const errors = arr.some((str) => !predicate(str)); 27 | if (errors) { 28 | throw new InvalidDecoratorItemException(decorator, item, context.name); 29 | } 30 | return true; 31 | } 32 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/trpc-panel.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | All, 3 | Controller, 4 | Inject, 5 | OnModuleInit, 6 | Response, 7 | } from '@nestjs/common'; 8 | import { renderTrpcPanel } from 'trpc-panel'; 9 | import type { AnyRouter } from '@trpc/server'; 10 | import { AppRouterHost } from 'nestjs-trpc'; 11 | import type { FastifyReply } from 'fastify'; 12 | 13 | @Controller() 14 | export class TrpcPanelController implements OnModuleInit { 15 | private appRouter!: AnyRouter; 16 | 17 | constructor( 18 | @Inject(AppRouterHost) private readonly appRouterHost: AppRouterHost, 19 | ) {} 20 | 21 | onModuleInit() { 22 | this.appRouter = this.appRouterHost.appRouter; 23 | } 24 | 25 | @All('/panel') 26 | panel(@Response() res: FastifyReply) { 27 | res.type('text/html').send( 28 | renderTrpcPanel(this.appRouter, { 29 | url: 'http://localhost:8080/trpc', 30 | }), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/nestjs-express/src/protected.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MiddlewareOptions, 3 | MiddlewareResponse, 4 | TRPCMiddleware, 5 | } from 'nestjs-trpc'; 6 | import { Inject, Injectable } from '@nestjs/common'; 7 | import { UserService } from './user.service'; 8 | import { TRPCError } from '@trpc/server'; 9 | 10 | @Injectable() 11 | export class ProtectedMiddleware implements TRPCMiddleware { 12 | constructor(@Inject(UserService) private readonly userService: UserService) {} 13 | async use(opts: MiddlewareOptions): Promise { 14 | const start = Date.now(); 15 | const result = await opts.next({ 16 | ctx: { 17 | ben: 1, 18 | }, 19 | }); 20 | 21 | const durationMs = Date.now() - start; 22 | const meta = { path: opts.path, type: opts.type, durationMs }; 23 | result.ok 24 | ? console.log('OK request timing:', meta) 25 | : console.error('Non-OK request timing', meta); 26 | return result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/generator.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassDeclaration, 3 | EnumDeclaration, 4 | Expression, 5 | FunctionDeclaration, 6 | InterfaceDeclaration, 7 | SourceFile, 8 | VariableDeclaration, 9 | } from 'ts-morph'; 10 | 11 | export interface RouterGeneratorMetadata { 12 | name: string; 13 | alias?: string; 14 | procedures: Array; 15 | } 16 | 17 | export interface ProcedureGeneratorMetadata { 18 | name: string; 19 | decorators: Array; 20 | } 21 | 22 | export interface DecoratorGeneratorMetadata { 23 | name: 'Query' | 'Mutation'; 24 | arguments: { 25 | input?: string; 26 | output?: string; 27 | }; 28 | } 29 | 30 | export interface SourceFileImportsMap { 31 | initializer: 32 | | Expression 33 | | ClassDeclaration 34 | | InterfaceDeclaration 35 | | EnumDeclaration 36 | | VariableDeclaration 37 | | FunctionDeclaration; 38 | sourceFile: SourceFile; 39 | } 40 | -------------------------------------------------------------------------------- /examples/nextjs-trpc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", // Extend the config options from the root, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@server/*": [ 26 | "../nestjs-express/src/*" 27 | ] 28 | }, 29 | "skipLibCheck": true, 30 | "forceConsistentCasingInFileNames": true, 31 | "incremental": true 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", 37 | ".next/types/**/*.ts" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/drivers/express.driver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import type { Application as ExpressApplication } from 'express'; 3 | import { TRPCContext, TRPCModuleOptions } from '../interfaces'; 4 | import type { AnyRouter } from '@trpc/server'; 5 | import * as trpcExpress from '@trpc/server/adapters/express'; 6 | 7 | @Injectable() 8 | export class ExpressDriver< 9 | TOptions extends Record = TRPCModuleOptions, 10 | > { 11 | public async start( 12 | options: TRPCModuleOptions, 13 | app: ExpressApplication, 14 | appRouter: AnyRouter, 15 | contextInstance: TRPCContext | null, 16 | ) { 17 | app.use( 18 | options.basePath ?? '/trpc', 19 | trpcExpress.createExpressMiddleware({ 20 | router: appRouter, 21 | ...(options.context != null && contextInstance != null 22 | ? { 23 | createContext: (opts) => contextInstance.create(opts), 24 | } 25 | : {}), 26 | }), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/public/icons/pnpm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | file_type_pnpm 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@17/schema/release-it.json", 3 | "git": { 4 | "commitMessage": "chore: release v${version}" 5 | }, 6 | "github": { 7 | "release": true 8 | }, 9 | "npm": false, 10 | "plugins": { 11 | "@release-it-plugins/workspaces": { 12 | "publish": true, 13 | "workspaces": ["packages/*"] 14 | }, 15 | "@release-it/conventional-changelog": { 16 | "infile": "CHANGELOG.md", 17 | "header": "# Changelog", 18 | "ignoreRecommendedBump": true, 19 | "preset": { 20 | "name": "conventionalcommits", 21 | "types": [ 22 | { 23 | "type": "feat", 24 | "section": "Features" 25 | }, 26 | { 27 | "type": "fix", 28 | "section": "Bug Fixes" 29 | }, 30 | { 31 | "type": "docs", 32 | "section": "Docs" 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/drivers/fastify.driver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import type { FastifyInstance as FastifyApplication } from 'fastify'; 3 | import { ContextOptions, TRPCContext, TRPCModuleOptions } from '../interfaces'; 4 | import type { AnyRouter } from '@trpc/server'; 5 | import * as trpcFastify from '@trpc/server/adapters/fastify'; 6 | 7 | @Injectable() 8 | export class FastifyDriver< 9 | TOptions extends Record = TRPCModuleOptions, 10 | > { 11 | public async start( 12 | options: TRPCModuleOptions, 13 | app: FastifyApplication, 14 | appRouter: AnyRouter, 15 | contextInstance: TRPCContext | null, 16 | ) { 17 | app.register(trpcFastify.fastifyTRPCPlugin, { 18 | prefix: options.basePath ?? '/trpc', 19 | trpcOptions: { 20 | router: appRouter, 21 | ...(options.context != null && contextInstance != null 22 | ? { 23 | createContext: (opts: ContextOptions) => 24 | contextInstance.create(opts), 25 | } 26 | : {}), 27 | }, 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/public/icons/yarn.svg: -------------------------------------------------------------------------------- 1 | 2 | file_type_yarn -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017-2022 Kevin Edry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /docs/public/icons/typescript.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/context.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProcedureParamDecorator, 3 | ProcedureParamDecoratorType, 4 | } from '../interfaces/factory.interface'; 5 | import { PROCEDURE_PARAM_METADATA_KEY } from '../trpc.constants'; 6 | 7 | export function Ctx(): ParameterDecorator { 8 | return ( 9 | target: object, 10 | propertyKey: string | symbol | undefined, 11 | parameterIndex?: number | TypedPropertyDescriptor, 12 | ) => { 13 | if (propertyKey != null && typeof parameterIndex === 'number') { 14 | const existingParams: Array = 15 | Reflect.getMetadata( 16 | PROCEDURE_PARAM_METADATA_KEY, 17 | target, 18 | propertyKey, 19 | ) || []; 20 | 21 | const procedureParamMetadata: ProcedureParamDecorator = { 22 | type: ProcedureParamDecoratorType.Ctx, 23 | index: parameterIndex, 24 | }; 25 | existingParams.push(procedureParamMetadata); 26 | Reflect.defineMetadata( 27 | PROCEDURE_PARAM_METADATA_KEY, 28 | existingParams, 29 | target, 30 | propertyKey, 31 | ); 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/query.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchema } from 'zod'; 2 | import { applyDecorators, SetMetadata } from '@nestjs/common'; 3 | import { PROCEDURE_METADATA_KEY, PROCEDURE_TYPE_KEY } from '../trpc.constants'; 4 | import { ProcedureType } from '../trpc.enum'; 5 | 6 | /** 7 | * Decorator that marks a router class method as a TRPC query procedure that can receive inbound 8 | * requests and produce responses. 9 | * 10 | * An TRPC query procedure is mainly responsible for actions that retrieve data. 11 | * for example `Query /trpc/userRouter.getUsers`. 12 | * 13 | * @param {object} args configuration object specifying: 14 | * - `input` - defines a `ZodSchema` validation logic for the input. 15 | * - `output` - defines a `ZodSchema` validation logic for the output. 16 | * 17 | * @see [Method Decorators](https://nestjs-trpc.io/docs/routers#procedures) 18 | * 19 | * @publicApi 20 | */ 21 | export function Query(args?: { input?: ZodSchema; output?: ZodSchema }) { 22 | return applyDecorators( 23 | ...[ 24 | SetMetadata(PROCEDURE_TYPE_KEY, ProcedureType.Query), 25 | SetMetadata(PROCEDURE_METADATA_KEY, args), 26 | ], 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/nestjs-express/src/user.router.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { 3 | Router, 4 | Query, 5 | UseMiddlewares, 6 | Input, 7 | Ctx, 8 | Options, 9 | ProcedureOptions, 10 | } from 'nestjs-trpc'; 11 | import { UserService } from './user.service'; 12 | import { ProtectedMiddleware } from './protected.middleware'; 13 | import { z } from 'zod'; 14 | import { TRPCError } from '@trpc/server'; 15 | import { User, userSchema } from './user.schema'; 16 | 17 | @Router({ alias: 'users' }) 18 | export class UserRouter { 19 | constructor(@Inject(UserService) private readonly userService: UserService) {} 20 | 21 | @Query({ 22 | input: z.object({ userId: z.string() }), 23 | output: userSchema, 24 | }) 25 | @UseMiddlewares(ProtectedMiddleware) 26 | async getUserById( 27 | @Input('userId') userId: string, 28 | @Ctx() ctx: object, 29 | @Options() opts: ProcedureOptions, 30 | ): Promise { 31 | const user = await this.userService.getUser(userId); 32 | 33 | if (!ctx) { 34 | throw new TRPCError({ 35 | message: 'Could not find user.', 36 | code: 'NOT_FOUND', 37 | }); 38 | } 39 | 40 | return user; 41 | } 42 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/mutation.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchema } from 'zod'; 2 | import { applyDecorators, SetMetadata } from '@nestjs/common'; 3 | import { PROCEDURE_METADATA_KEY, PROCEDURE_TYPE_KEY } from '../trpc.constants'; 4 | import { ProcedureType } from '../trpc.enum'; 5 | 6 | /** 7 | * Decorator that marks a router class method as a TRPC mutation procedure that can receive inbound 8 | * requests and produce responses. 9 | * 10 | * An TRPC query procedure is mainly responsible for actions that modify or creates server-side data. 11 | * for example `Mutation /trpc/userRouter.createUser`. 12 | * 13 | * @param {object} args configuration object specifying: 14 | * - `input` - defines a `ZodSchema` validation logic for the input. 15 | * - `output` - defines a `ZodSchema` validation logic for the output. 16 | * 17 | * @see [Method Decorators](https://nestjs-trpc.io/docs/routers#procedures) 18 | * 19 | * @publicApi 20 | */ 21 | export function Mutation(args?: { input?: ZodSchema; output?: ZodSchema }) { 22 | return applyDecorators( 23 | ...[ 24 | SetMetadata(PROCEDURE_TYPE_KEY, ProcedureType.Mutation), 25 | SetMetadata(PROCEDURE_METADATA_KEY, args), 26 | ], 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-trpc.io", 3 | "version": "1.0.0", 4 | "main": ".next/index.js", 5 | "scripts": { 6 | "build": "rimraf .next && rimraf .out && next build && next-sitemap", 7 | "start": "next start", 8 | "dev": "next dev", 9 | "lint": "next lint" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@stackblitz/sdk": "^1.11.0", 16 | "@uidotdev/usehooks": "^2.4.1", 17 | "@vercel/analytics": "^1.3.1", 18 | "clsx": "^2.1.1", 19 | "framer-motion": "^11.2.13", 20 | "geist": "^1.3.0", 21 | "lucide-react": "^0.401.0", 22 | "next": "14.2.4", 23 | "nextra": "^2.13.4", 24 | "nextra-theme-docs": "^2.13.4", 25 | "react": "^18", 26 | "react-dom": "^18" 27 | }, 28 | "devDependencies": { 29 | "@svgr/webpack": "^8.1.0", 30 | "@types/node": "^20.14.10", 31 | "@types/react": "^18", 32 | "@types/react-dom": "^18", 33 | "autoprefixer": "^10.4.19", 34 | "eslint": "^9.9.1", 35 | "eslint-config-next": "^14.2.6", 36 | "next-sitemap": "^4.2.3", 37 | "postcss": "^8", 38 | "rimraf": "^6.0.1", 39 | "tailwindcss": "^3.4.1", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/factories/__tests__/trpc.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TRPCFactory } from '../trpc.factory'; 3 | import { TRPCGenerator } from '../../generators/trpc.generator'; 4 | import { RouterFactory } from '../router.factory'; 5 | import { ProcedureFactory } from '../procedure.factory'; 6 | 7 | describe('TRPCFactory', () => { 8 | let trpcFactory: TRPCFactory; 9 | let routerFactory: RouterFactory; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [ 14 | TRPCFactory, 15 | { 16 | provide: TRPCGenerator, 17 | useValue: {}, 18 | }, 19 | { 20 | provide: RouterFactory, 21 | useValue: { 22 | serializeRoutes: jest.fn(), 23 | }, 24 | }, 25 | { 26 | provide: ProcedureFactory, 27 | useValue: {}, 28 | }, 29 | ], 30 | }).compile(); 31 | 32 | trpcFactory = module.get(TRPCFactory); 33 | routerFactory = module.get(RouterFactory); 34 | }); 35 | 36 | it('should be defined', () => { 37 | expect(trpcFactory).toBeDefined(); 38 | }); 39 | }); -------------------------------------------------------------------------------- /docs/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require('nextra')({ 2 | theme: 'nextra-theme-docs', 3 | themeConfig: './theme.config.jsx' 4 | }) 5 | module.exports = withNextra({ 6 | transpilePackages: ['geist'], 7 | webpack(config) { 8 | // Grab the existing rule that handles SVG imports 9 | const fileLoaderRule = config.module.rules.find((rule) => 10 | rule.test?.test?.('.svg'), 11 | ) 12 | 13 | config.module.rules.push( 14 | // Reapply the existing rule, but only for svg imports ending in ?url 15 | { 16 | ...fileLoaderRule, 17 | test: /\.svg$/i, 18 | resourceQuery: /url/, // *.svg?url 19 | }, 20 | // Convert all other *.svg imports to React components 21 | { 22 | test: /\.svg$/i, 23 | issuer: fileLoaderRule.issuer, 24 | resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url 25 | use: ['@svgr/webpack'], 26 | }, 27 | ) 28 | 29 | // Modify the file loader rule to ignore *.svg, since we have it handled now. 30 | fileLoaderRule.exclude = /\.svg$/i 31 | 32 | return config 33 | } 34 | }) 35 | 36 | 37 | // If you have other Next.js configurations, you can pass them as the parameter: 38 | // module.exports = withNextra({ /* other next.js config */ }) -------------------------------------------------------------------------------- /examples/nestjs-fastify/src/user.router.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { 3 | Router, 4 | Query, 5 | UseMiddlewares, 6 | Input, 7 | Ctx, 8 | Options, 9 | ProcedureOptions, 10 | } from 'nestjs-trpc'; 11 | import { UserService } from './user.service'; 12 | import { ProtectedMiddleware } from './protected.middleware'; 13 | import { z } from 'zod'; 14 | import { TRPCError } from '@trpc/server'; 15 | import { User, userSchema } from './user.schema'; 16 | import { LoggingMiddleware } from './logging.middleware'; 17 | 18 | @UseMiddlewares(LoggingMiddleware) 19 | @Router({ alias: 'users' }) 20 | export class UserRouter { 21 | constructor(@Inject(UserService) private readonly userService: UserService) {} 22 | 23 | @Query({ 24 | input: z.object({ userId: z.string() }), 25 | output: userSchema, 26 | }) 27 | @UseMiddlewares(ProtectedMiddleware) 28 | async getUserById( 29 | @Input('userId') userId: string, 30 | @Ctx() ctx: object, 31 | @Options() opts: ProcedureOptions, 32 | ): Promise { 33 | const user = await this.userService.getUser(userId); 34 | 35 | if (!ctx) { 36 | throw new TRPCError({ 37 | message: 'Could not find user.', 38 | code: 'NOT_FOUND', 39 | }); 40 | } 41 | 42 | return user; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/router.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, SetMetadata } from '@nestjs/common'; 2 | import { ROUTER_METADATA_KEY } from '../trpc.constants'; 3 | import { FileScanner } from '../scanners/file.scanner'; 4 | 5 | const fileScanner = new FileScanner(); 6 | 7 | /** 8 | * Decorator that marks a class as a TRPC router that can receive inbound 9 | * requests and produce responses. 10 | * 11 | * An TRPC Router responds to inbound HTTP Requests and produces HTTP Responses. 12 | * It defines a class that provides the context for one or more related procedures that correspond to HTTP request methods and associated routes 13 | * for example `Query /trpc/userRouter.getUsers`, `Mutation /trpc/userRouter.createUser`. 14 | * 15 | * 16 | * @param {object} args configuration object specifying: 17 | * - `alias` - string that defines a router alias. The alias is used both in the auto schema file generation, and for the actual api access. 18 | * 19 | * @see [Routers](https://nestjs-trpc.io/docs/routers) 20 | * 21 | * @publicApi 22 | */ 23 | export function Router(args?: { alias?: string }): ClassDecorator { 24 | const path = fileScanner.getCallerFilePath(); 25 | return applyDecorators( 26 | ...[SetMetadata(ROUTER_METADATA_KEY, { alias: args?.alias, path })], 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/path.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProcedureParamDecorator, 3 | ProcedureParamDecoratorType, 4 | } from '../interfaces/factory.interface'; 5 | import { PROCEDURE_PARAM_METADATA_KEY } from '../trpc.constants'; 6 | 7 | /** 8 | * Path procedure parameter decorator. Extracts the `path` parameter out of the procedure `opts`. 9 | * 10 | * @see [Parameter Decorators](https://www.nestjs-trpc.io/docs/routers#parameter-decorators) 11 | * 12 | * @publicApi 13 | */ 14 | export function Path(): ParameterDecorator { 15 | return ( 16 | target: object, 17 | propertyKey: string | symbol | undefined, 18 | parameterIndex: number, 19 | ) => { 20 | if (propertyKey != null) { 21 | const existingParams: Array = 22 | Reflect.getMetadata( 23 | PROCEDURE_PARAM_METADATA_KEY, 24 | target, 25 | propertyKey, 26 | ) || []; 27 | 28 | const procedureParamMetadata: ProcedureParamDecorator = { 29 | type: ProcedureParamDecoratorType.Path, 30 | index: parameterIndex, 31 | }; 32 | existingParams.push(procedureParamMetadata); 33 | Reflect.defineMetadata( 34 | PROCEDURE_PARAM_METADATA_KEY, 35 | existingParams, 36 | target, 37 | propertyKey, 38 | ); 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/type.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProcedureParamDecorator, 3 | ProcedureParamDecoratorType, 4 | } from '../interfaces/factory.interface'; 5 | import { PROCEDURE_PARAM_METADATA_KEY } from '../trpc.constants'; 6 | 7 | /** 8 | * Type procedure parameter decorator. Extracts the `type` parameter out of the procedure `opts`. 9 | * 10 | * @see [Parameter Decorators](https://www.nestjs-trpc.io/docs/routers#parameter-decorators) 11 | * 12 | * @publicApi 13 | */ 14 | export function Type(): ParameterDecorator { 15 | return ( 16 | target: object, 17 | propertyKey: string | symbol | undefined, 18 | parameterIndex: number, 19 | ) => { 20 | if (propertyKey != null) { 21 | const existingParams: Array = 22 | Reflect.getMetadata( 23 | PROCEDURE_PARAM_METADATA_KEY, 24 | target, 25 | propertyKey, 26 | ) || []; 27 | 28 | const procedureParamMetadata: ProcedureParamDecorator = { 29 | type: ProcedureParamDecoratorType.Type, 30 | index: parameterIndex, 31 | }; 32 | existingParams.push(procedureParamMetadata); 33 | Reflect.defineMetadata( 34 | PROCEDURE_PARAM_METADATA_KEY, 35 | existingParams, 36 | target, 37 | propertyKey, 38 | ); 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/options.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProcedureParamDecorator, 3 | ProcedureParamDecoratorType, 4 | } from '../interfaces/factory.interface'; 5 | import { PROCEDURE_PARAM_METADATA_KEY } from '../trpc.constants'; 6 | 7 | /** 8 | * Options procedure parameter decorator. Extracts the root `opts` parameter out of the procedure. 9 | * 10 | * @see [Parameter Decorators](https://www.nestjs-trpc.io/docs/routers#parameter-decorators) 11 | * 12 | * @publicApi 13 | */ 14 | export function Options(): ParameterDecorator { 15 | return ( 16 | target: object, 17 | propertyKey: string | symbol | undefined, 18 | parameterIndex: number, 19 | ) => { 20 | if (propertyKey != null) { 21 | const existingParams: Array = 22 | Reflect.getMetadata( 23 | PROCEDURE_PARAM_METADATA_KEY, 24 | target, 25 | propertyKey, 26 | ) || []; 27 | 28 | const procedureParamMetadata: ProcedureParamDecorator = { 29 | type: ProcedureParamDecoratorType.Options, 30 | index: parameterIndex, 31 | }; 32 | existingParams.push(procedureParamMetadata); 33 | Reflect.defineMetadata( 34 | PROCEDURE_PARAM_METADATA_KEY, 35 | existingParams, 36 | target, 37 | propertyKey, 38 | ); 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /docs/pages/docs/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "-- getting-started": { 3 | "type": "separator", 4 | "title": "Getting Started" 5 | }, 6 | "index": { 7 | "title": "Installation" 8 | }, 9 | 10 | "-- overview": { 11 | "type": "separator", 12 | "title": "Overview" 13 | }, 14 | "nestjs": "NestJS Quickstart", 15 | "trpc": "tRPC Concepts", 16 | "graphql": "GraphQL Migration", 17 | 18 | "-- features": { 19 | "type": "separator", 20 | "title": "Usage Guide" 21 | }, 22 | 23 | "structure": "First Steps", 24 | "routers": "Routers", 25 | "middlewares": "Middlewares", 26 | "context": "Context", 27 | "dependency-injection": "Dependency Injection", 28 | "client": "Client Usage", 29 | "integrations": "Integrations", 30 | 31 | "-- appendix": { 32 | "type": "separator", 33 | "title": "Appendix" 34 | }, 35 | 36 | "trpc-docs": { 37 | "title": "⇲ tRPC Documentation", 38 | "href": "https://trpc.io/docs", 39 | "newWindow": true 40 | }, 41 | "nestjs-docs": { 42 | "title": "⇲ NestJS Documentation", 43 | "href": "https://docs.nestjs.com", 44 | "newWindow": true 45 | }, 46 | "docs": { 47 | "title": "Docs", 48 | "type": "page" 49 | }, 50 | "contact": { 51 | "title": "Contact ↗", 52 | "type": "page", 53 | "href": "https://twitter.com/KevinEdry", 54 | "newWindow": true 55 | } 56 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/raw-input.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProcedureParamDecorator, 3 | ProcedureParamDecoratorType, 4 | } from '../interfaces/factory.interface'; 5 | import { PROCEDURE_PARAM_METADATA_KEY } from '../trpc.constants'; 6 | 7 | /** 8 | * Raw Input procedure parameter decorator. Extracts the `rawInput` parameter out of the procedure `opts`. 9 | * 10 | * @see [Parameter Decorators](https://www.nestjs-trpc.io/docs/routers#parameter-decorators) 11 | * 12 | * @publicApi 13 | */ 14 | export function RawInput(): ParameterDecorator { 15 | return ( 16 | target: object, 17 | propertyKey: string | symbol | undefined, 18 | parameterIndex: number, 19 | ) => { 20 | if (propertyKey != null) { 21 | const existingParams: Array = 22 | Reflect.getMetadata( 23 | PROCEDURE_PARAM_METADATA_KEY, 24 | target, 25 | propertyKey, 26 | ) || []; 27 | 28 | const procedureParamMetadata: ProcedureParamDecorator = { 29 | type: ProcedureParamDecoratorType.RawInput, 30 | index: parameterIndex, 31 | }; 32 | existingParams.push(procedureParamMetadata); 33 | Reflect.defineMetadata( 34 | PROCEDURE_PARAM_METADATA_KEY, 35 | existingParams, 36 | target, 37 | propertyKey, 38 | ); 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /examples/nestjs-fastify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-nestjs-fastify", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/main.ts", 6 | "scripts": { 7 | "build": "nest build", 8 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 9 | "start": "nest start", 10 | "start:dev": "nest start --watch --preserveWatchOutput", 11 | "start:debug": "nest start --debug --watch --preserveWatchOutput", 12 | "start:prod": "node dist/main" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@nestjs/common": "10.4.1", 19 | "@nestjs/config": "^3.0.0", 20 | "@nestjs/core": "10.4.1", 21 | "@nestjs/platform-fastify": "^10.4.4", 22 | "@trpc/server": "^10.33.0", 23 | "fastify": "^5.0.0", 24 | "nestjs-trpc": "workspace:*", 25 | "reflect-metadata": "^0.1.13", 26 | "rxjs": "^7.2.0", 27 | "zod": "^3.21.4" 28 | }, 29 | "devDependencies": { 30 | "@nestjs/cli": "10.4.4", 31 | "@nestjs/schematics": "^10.1.2", 32 | "@swc/cli": "^0.4.0", 33 | "@swc/core": "^1.7.18", 34 | "@types/node": "^22.5.0", 35 | "chokidar": "^3.6.0", 36 | "trpc-panel": "^1.3.4", 37 | "ts-morph": "^23.0.0", 38 | "ts-node": "^10.9.1", 39 | "tsconfig-paths": "^4.2.0", 40 | "tslib": "^2.5.0", 41 | "typescript": "^5.0.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/nestjs-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-nestjs-express", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/main.ts", 6 | "scripts": { 7 | "build": "nest build", 8 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 9 | "start": "nest start", 10 | "start:dev": "nest start --watch --preserveWatchOutput", 11 | "start:debug": "nest start --debug --watch --preserveWatchOutput", 12 | "start:prod": "node dist/main" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@nestjs/common": "10.4.1", 19 | "@nestjs/config": "^3.0.0", 20 | "@nestjs/core": "10.4.1", 21 | "@nestjs/platform-express": "10.4.1", 22 | "@trpc/server": "^10.33.0", 23 | "nestjs-trpc": "workspace:*", 24 | "reflect-metadata": "^0.1.13", 25 | "rxjs": "^7.2.0", 26 | "zod": "^3.21.4" 27 | }, 28 | "devDependencies": { 29 | "@nestjs/cli": "10.4.4", 30 | "@nestjs/schematics": "^10.1.2", 31 | "@swc/cli": "^0.4.0", 32 | "@swc/core": "^1.7.18", 33 | "@types/express": "^4.17.17", 34 | "@types/node": "^22.5.0", 35 | "chokidar": "^3.6.0", 36 | "trpc-panel": "^1.3.4", 37 | "ts-morph": "^23.0.0", 38 | "ts-node": "^10.9.1", 39 | "tsconfig-paths": "^4.2.0", 40 | "tslib": "^2.5.0", 41 | "typescript": "^5.0.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | 5 | export const Footer = () => { 6 | return ( 7 | 27 | ); 28 | }; -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/input.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProcedureParamDecorator, 3 | ProcedureParamDecoratorType, 4 | } from '../interfaces/factory.interface'; 5 | import { PROCEDURE_PARAM_METADATA_KEY } from '../trpc.constants'; 6 | 7 | /** 8 | * Input procedure parameter decorator. Extracts the `input` parameter out of the procedure `opts`. 9 | * 10 | * @param key string to be used extracting a specific input key - `input[key]`. 11 | * 12 | * @see [Parameter Decorators](https://www.nestjs-trpc.io/docs/routers#parameter-decorators) 13 | * 14 | * @publicApi 15 | */ 16 | export function Input(key?: string): ParameterDecorator { 17 | return ( 18 | target: object, 19 | propertyKey: string | symbol | undefined, 20 | parameterIndex: number, 21 | ) => { 22 | if (propertyKey != null && typeof parameterIndex === 'number') { 23 | const existingParams: Array = 24 | Reflect.getMetadata( 25 | PROCEDURE_PARAM_METADATA_KEY, 26 | target, 27 | propertyKey, 28 | ) || []; 29 | 30 | const procedureParamMetadata: ProcedureParamDecorator = { 31 | type: ProcedureParamDecoratorType.Input, 32 | index: parameterIndex, 33 | key, 34 | }; 35 | existingParams.push(procedureParamMetadata); 36 | Reflect.defineMetadata( 37 | PROCEDURE_PARAM_METADATA_KEY, 38 | existingParams, 39 | target, 40 | propertyKey, 41 | ); 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /docs/public/icons/trpc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/components/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import { Info } from 'lucide-react'; 2 | import React, { ReactNode } from 'react'; 3 | 4 | interface TableProps { 5 | children: ReactNode; 6 | columns?: string[]; 7 | } 8 | 9 | interface CellProps { 10 | children: ReactNode; 11 | description?: string; 12 | } 13 | 14 | function Table({ children, columns = [] }: TableProps) { 15 | return ( 16 | 17 | 18 | 19 | {columns.map((column, index) => ( 20 | {column} 21 | ))} 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | ); 29 | } 30 | 31 | function Column({ children }: { children: ReactNode }) { 32 | return {children}; 33 | } 34 | 35 | function Row({ children }: { children: ReactNode }) { 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | 43 | function Cell({ children, description }: CellProps) { 44 | return ( 45 | 46 | 47 | {children} 48 | {description != null && } 49 | 50 | 51 | ); 52 | } 53 | 54 | // Attaching subcomponents to Table 55 | Table.Column = Column; 56 | Table.Row = Row; 57 | Table.Cell = Cell; 58 | 59 | export default Table; 60 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/factories/middleware.factory.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { Class, Constructor } from 'type-fest'; 3 | import { TRPCMiddleware } from '../interfaces'; 4 | import { RouterFactory } from './router.factory'; 5 | import { ProcedureFactory } from './procedure.factory'; 6 | import { isEqual, uniqWith } from 'lodash'; 7 | 8 | interface MiddlewareMetadata { 9 | path: string; 10 | instance: Class | Constructor; 11 | } 12 | 13 | @Injectable() 14 | export class MiddlewareFactory { 15 | @Inject(RouterFactory) 16 | private readonly routerFactory!: RouterFactory; 17 | 18 | @Inject(ProcedureFactory) 19 | private readonly procedureFactory!: ProcedureFactory; 20 | 21 | getMiddlewares(): Array { 22 | const routers = this.routerFactory.getRouters(); 23 | 24 | const middlewaresMetadata = routers.flatMap((router) => { 25 | const { instance, middlewares, path } = router; 26 | const prototype = Object.getPrototypeOf(instance); 27 | const procedures = this.procedureFactory.getProcedures( 28 | instance, 29 | prototype, 30 | ); 31 | 32 | const procedureMiddleware = procedures.flatMap((procedure) => { 33 | return procedure.middlewares ?? []; 34 | }); 35 | 36 | return [...middlewares, ...procedureMiddleware].map((middleware) => ({ 37 | path, 38 | instance: middleware, 39 | })); 40 | }); 41 | 42 | // Return a unique array of middlewares based on both path and instances 43 | return uniqWith(middlewaresMetadata, isEqual); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/middlewares.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { UseMiddlewares } from '../middlewares.decorator'; 3 | import { MIDDLEWARES_KEY } from '../../trpc.constants'; 4 | import { MiddlewareOptions, MiddlewareResponse, TRPCMiddleware } from '../../interfaces'; 5 | 6 | describe('UseMiddlewares Decorator', () => { 7 | class TestMiddleware implements TRPCMiddleware { 8 | use(opts: MiddlewareOptions): MiddlewareResponse | Promise { 9 | throw new Error('Method not implemented.'); 10 | } 11 | } 12 | 13 | it('should add metadata to the class', () => { 14 | @UseMiddlewares(TestMiddleware) 15 | class TestClass {} 16 | 17 | const metadata = Reflect.getMetadata(MIDDLEWARES_KEY, TestClass); 18 | expect(metadata).toStrictEqual([TestMiddleware]); 19 | }); 20 | 21 | it('should add metadata to the method', () => { 22 | class TestClass { 23 | @UseMiddlewares(TestMiddleware) 24 | testMethod() {} 25 | } 26 | 27 | const metadata = Reflect.getMetadata(MIDDLEWARES_KEY, TestClass.prototype.testMethod); 28 | expect(metadata).toStrictEqual([TestMiddleware]); 29 | }); 30 | 31 | it('should throw an error for invalid middleware on class', () => { 32 | expect(() => { 33 | @UseMiddlewares({} as any) 34 | class TestClass {} 35 | }).toThrow(); 36 | }); 37 | 38 | it('should throw an error for invalid middleware on method', () => { 39 | expect(() => { 40 | class TestClass { 41 | @UseMiddlewares({} as any) 42 | testMethod() {} 43 | } 44 | }).toThrow(); 45 | }); 46 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/router.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Router } from '../router.decorator'; 3 | import { ROUTER_METADATA_KEY } from '../../trpc.constants'; 4 | 5 | describe('Router Decorator', () => { 6 | it('should set router metadata without alias', () => { 7 | @Router() 8 | class TestRouter {} 9 | 10 | const metadata = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter); 11 | expect(metadata).toStrictEqual({alias: undefined, path: __filename}) 12 | }); 13 | 14 | it('should set router metadata with alias', () => { 15 | const alias = 'testAlias'; 16 | 17 | @Router({ alias }) 18 | class TestRouter {} 19 | 20 | const metadata = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter); 21 | expect(metadata).toStrictEqual({alias, path: __filename}) 22 | }); 23 | 24 | it('should not affect class methods', () => { 25 | @Router() 26 | class TestRouter { 27 | testMethod() {} 28 | } 29 | 30 | const metadata = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter.prototype.testMethod); 31 | expect(metadata).toBeUndefined(); 32 | }); 33 | 34 | it('should allow multiple routers with different aliases', () => { 35 | @Router({ alias: 'router1' }) 36 | class TestRouter1 {} 37 | 38 | @Router({ alias: 'router2' }) 39 | class TestRouter2 {} 40 | 41 | const metadata1 = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter1); 42 | const metadata2 = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter2); 43 | 44 | expect(metadata1).toEqual({ alias: 'router1', path: __filename }); 45 | expect(metadata2).toEqual({ alias: 'router2', path: __filename }); 46 | }); 47 | }); -------------------------------------------------------------------------------- /docs/pages/docs/dependency-injection.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC Documentation - Dependency Injection" 3 | --- 4 | 5 | import { Callout } from 'nextra/components'; 6 | import Link from 'next/link'; 7 | 8 | # Dependency injection 9 | 10 | NestJS is built around the strong design pattern commonly known as Dependency injection. 11 | 12 | 13 | If you are new to NestJS and/or the concept of Dependency Injection, the NestJS official docs recommends to read this article about dependency injection from the angular official documentation. 14 | 15 | 16 | >NestJS Official Documentation: 17 | >Thanks to TypeScript capabilities, it's extremely easy to manage dependencies because they are resolved just by type. 18 | >In the example below, Nest will resolve the catsService by creating and returning an instance of CatsService (or, in the normal case of a singleton, returning the existing instance if it has already been requested elsewhere). 19 | >This dependency is resolved and passed to your router's constructor (or assigned to the indicated property) 20 | 21 | With this adapter, it is possible to use dependency injection with you routers, middlewares or context providers with ease. 22 | The adapter will automatically resolve the dependency and will inject the instance when invoking the method. 23 | To do that we use the `@Inject()` decorator provided by NestJS. You can read more about it in their official documentation. 24 | 25 | ```typescript /Inject/ 26 | constructor(@Inject(CatsService) private catsService: CatsService) {} 27 | ``` -------------------------------------------------------------------------------- /docs/components/Preview/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import type { ComponentPropsWithoutRef } from 'react'; 3 | import React, { useState } from 'react'; 4 | import { Iframe } from '../Iframe'; 5 | import { searchParams } from '../../utils/searchParams'; 6 | import Image from 'next/image'; 7 | 8 | export const Preview = ( 9 | props: Omit, 'className'>, 10 | ) => { 11 | const [loaded, setLoaded] = useState(false); 12 | return ( 13 | <> 14 | 18 | 23 | 38 | 39 | > 40 | ); 41 | }; -------------------------------------------------------------------------------- /docs/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --bg: #090909; 7 | --bg-contrast: #181818; 8 | --bg-contrast-light: #262626; 9 | } 10 | 11 | body { 12 | background: var(--bg) !important; 13 | } 14 | 15 | /* Code block Header */ 16 | .nextra-code-block > :is(html[class~=dark] .dark\:nx-text-gray-200) { 17 | background: var(--bg-contrast-light) !important; 18 | font-size: small; 19 | } 20 | 21 | /* Code block Body */ 22 | :is(html[class~=dark] .dark\:nx-bg-primary-300\/10) { 23 | background: var(--bg-contrast); 24 | border: 1px solid var(--bg-contrast-light); 25 | } 26 | 27 | /* Tab Key */ 28 | button:is(html[class~=dark] .dark\:nx-text-gray-200) { 29 | color: rgba(163,163,163,var(--tw-text-opacity)) !important; 30 | } 31 | 32 | /* Tab Key Selected */ 33 | button[aria-selected="true"] { 34 | color: white !important; 35 | border-color: white; 36 | outline: none !important; 37 | } 38 | 39 | /* Navbar bottom addons */ 40 | :is(html[class~=dark] .dark\:nx-shadow-\[0_-12px_16px_\#111\]) { 41 | background: transparent; 42 | box-shadow: none; 43 | } 44 | 45 | /* Warning Callout */ 46 | :is(html[class~=dark] .dark\:nx-text-yellow-200) { 47 | background: transparent; 48 | border: 1px solid var(--bg-contrast-light); 49 | color: rgba(163,163,163,var(--tw-text-opacity)); 50 | } 51 | /* Default Callout */ 52 | :is(html[class~=dark] .dark\:nx-text-orange-300) { 53 | background: transparent; 54 | border: 1px solid var(--bg-contrast-light); 55 | color: rgba(163,163,163,var(--tw-text-opacity)); 56 | } 57 | 58 | /*Header*/ 59 | :is(html[class~=dark] .dark\:nx-shadow-\[0_-1px_0_rgba\(255\,255\,255\,\.1\)_inset\]){ 60 | background: transparent !important; 61 | } 62 | 63 | .router-cards > .nextra-cards { 64 | --rows: 2 !important; 65 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [{ 18 | ignores: [ 19 | "packages/**/tests/**", 20 | "packages/**/dist/**", 21 | "examples", 22 | "dist", 23 | "node_modules", 24 | ".pnp.cjs", 25 | ".pnp.loader.mjs" 26 | ], 27 | }, ...compat.extends( 28 | "plugin:@typescript-eslint/eslint-recommended", 29 | "plugin:@typescript-eslint/recommended", 30 | "plugin:prettier/recommended", 31 | ), { 32 | plugins: { 33 | "@typescript-eslint": typescriptEslintEslintPlugin, 34 | }, 35 | 36 | languageOptions: { 37 | globals: { 38 | ...globals.node, 39 | ...globals.jest, 40 | }, 41 | 42 | parser: tsParser, 43 | ecmaVersion: 5, 44 | sourceType: "module", 45 | 46 | parserOptions: { 47 | project: "packages/tsconfig.build.json", 48 | }, 49 | }, 50 | 51 | rules: { 52 | "@typescript-eslint/interface-name-prefix": "off", 53 | "@typescript-eslint/explicit-function-return-type": "off", 54 | "@typescript-eslint/no-explicit-any": "off", 55 | "@typescript-eslint/no-use-before-define": "off", 56 | "@typescript-eslint/no-unused-vars": "off", 57 | "@typescript-eslint/explicit-module-boundary-types": "off", 58 | "@typescript-eslint/ban-types": "off", 59 | "@typescript-eslint/no-empty-function": "off", 60 | }, 61 | }]; -------------------------------------------------------------------------------- /docs/pages/docs/nestjs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC Documentation - NestJS Quickstart Guide" 3 | --- 4 | 5 | import {Steps} from 'nextra/components'; 6 | import Link from 'next/link' 7 | 8 | # NestJS Quickstart Guide 9 | NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It uses TypeScript by default, which enhances productivity and maintainability. NestJS incorporates elements from Object-Oriented Programming, Functional Programming, and Functional Reactive Programming. 10 | 11 | ### Why Choose NestJS? 12 | 13 | - **Modular Architecture**: Facilitates code organization and reuse. 14 | - **TypeScript Support**: Out-of-the-box TypeScript support ensures type safety and development efficiency. 15 | - **Dependency Injection**: Built-in dependency injection for better application structure and testing. 16 | - **Ecosystem**: A rich ecosystem of tools and libraries, including support for GraphQL, WebSockets, and more. 17 | 18 | ### Official Documentation 19 | This guide goes through the basics of NestJS. for a more indepth look, check out their official documentation. 20 | 21 | 22 | ### Installation 23 | 24 | To start using NestJS, you need to install the CLI: 25 | 26 | ```bash filename="Terminal" copy 27 | npm i -g @nestjs/cli 28 | ``` 29 | 30 | ### Creating a New Project 31 | 32 | Generate a new NestJS project using the CLI: 33 | 34 | ```bash filename="Terminal" copy 35 | nest new project-name 36 | ``` 37 | 38 | ### Running the Application 39 | 40 | Navigate to the project directory and start the application: 41 | 42 | ```bash filename="Terminal" copy 43 | cd project-name 44 | npm run start 45 | ``` 46 | 47 | 48 | Awesome! You should be all set to integrate **NestJS tRPC** into your application! 🎉 -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/type.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Type } from '../type.decorator'; 3 | import { PROCEDURE_PARAM_METADATA_KEY } from '../../trpc.constants'; 4 | import { ProcedureParamDecoratorType } from '../../interfaces/factory.interface'; 5 | 6 | describe('Type Decorator', () => { 7 | class TestClass { 8 | testMethod(@Type() param1: any, @Type() param2: any) {} 9 | } 10 | 11 | it('should add metadata to the method', () => { 12 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 13 | expect(metadata).toBeDefined(); 14 | expect(Array.isArray(metadata)).toBe(true); 15 | }); 16 | 17 | it('should add correct metadata for each parameter', () => { 18 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 19 | expect(metadata).toHaveLength(2); 20 | 21 | expect(metadata[0]).toEqual({ 22 | type: ProcedureParamDecoratorType.Type, 23 | index: 1, 24 | }); 25 | 26 | expect(metadata[1]).toEqual({ 27 | type: ProcedureParamDecoratorType.Type, 28 | index: 0, 29 | }); 30 | }); 31 | 32 | it('should append to existing metadata', () => { 33 | class TestClassWithExistingMetadata { 34 | testMethod(@Type() param1: any) {} 35 | } 36 | 37 | // Simulate existing metadata 38 | const existingMetadata = [{ type: 'SomeOtherDecorator', index: 0 }]; 39 | Reflect.defineMetadata(PROCEDURE_PARAM_METADATA_KEY, existingMetadata, TestClassWithExistingMetadata.prototype, 'testMethod'); 40 | 41 | // Apply our decorator 42 | Type()(TestClassWithExistingMetadata.prototype, 'testMethod', 1); 43 | 44 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClassWithExistingMetadata.prototype, 'testMethod'); 45 | expect(metadata).toHaveLength(2); 46 | expect(metadata[1]).toEqual({ 47 | type: ProcedureParamDecoratorType.Type, 48 | index: 1, 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/path.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Path } from '../path.decorator'; 3 | import { PROCEDURE_PARAM_METADATA_KEY } from '../../trpc.constants'; 4 | import { ProcedureParamDecoratorType } from '../../interfaces/factory.interface'; 5 | 6 | describe('Path Decorator', () => { 7 | class TestClass { 8 | testMethod(@Path() param1: string, @Path() param2: string) {} 9 | } 10 | 11 | it('should add metadata to the method', () => { 12 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 13 | expect(metadata).toBeDefined(); 14 | expect(Array.isArray(metadata)).toBe(true); 15 | }); 16 | 17 | it('should add correct metadata for each parameter', () => { 18 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 19 | expect(metadata).toHaveLength(2); 20 | 21 | expect(metadata[0]).toEqual({ 22 | type: ProcedureParamDecoratorType.Path, 23 | index: 1, 24 | }); 25 | 26 | expect(metadata[1]).toEqual({ 27 | type: ProcedureParamDecoratorType.Path, 28 | index: 0, 29 | }); 30 | }); 31 | 32 | it('should append to existing metadata', () => { 33 | class TestClassWithExistingMetadata { 34 | testMethod(@Path() param1: string) {} 35 | } 36 | 37 | // Simulate existing metadata 38 | const existingMetadata = [{ type: 'SomeOtherDecorator', index: 0 }]; 39 | Reflect.defineMetadata(PROCEDURE_PARAM_METADATA_KEY, existingMetadata, TestClassWithExistingMetadata.prototype, 'testMethod'); 40 | 41 | // Apply our decorator 42 | Path()(TestClassWithExistingMetadata.prototype, 'testMethod', 1); 43 | 44 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClassWithExistingMetadata.prototype, 'testMethod'); 45 | expect(metadata).toHaveLength(2); 46 | expect(metadata[1]).toEqual({ 47 | type: ProcedureParamDecoratorType.Path, 48 | index: 1, 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/options.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Options } from '../options.decorator'; 3 | import { PROCEDURE_PARAM_METADATA_KEY } from '../../trpc.constants'; 4 | import { ProcedureParamDecoratorType } from '../../interfaces/factory.interface'; 5 | 6 | describe('Options Decorator', () => { 7 | class TestClass { 8 | testMethod(@Options() param1: any, @Options() param2: any) {} 9 | } 10 | 11 | it('should add metadata to the method', () => { 12 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 13 | expect(metadata).toBeDefined(); 14 | expect(Array.isArray(metadata)).toBe(true); 15 | }); 16 | 17 | it('should add correct metadata for each parameter', () => { 18 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 19 | expect(metadata).toHaveLength(2); 20 | 21 | expect(metadata[0]).toEqual({ 22 | type: ProcedureParamDecoratorType.Options, 23 | index: 1, 24 | }); 25 | 26 | expect(metadata[1]).toEqual({ 27 | type: ProcedureParamDecoratorType.Options, 28 | index: 0, 29 | }); 30 | }); 31 | 32 | it('should append to existing metadata', () => { 33 | class TestClassWithExistingMetadata { 34 | testMethod(@Options() param1: any) {} 35 | } 36 | 37 | // Simulate existing metadata 38 | const existingMetadata = [{ type: 'SomeOtherDecorator', index: 0 }]; 39 | Reflect.defineMetadata(PROCEDURE_PARAM_METADATA_KEY, existingMetadata, TestClassWithExistingMetadata.prototype, 'testMethod'); 40 | 41 | // Apply our decorator 42 | Options()(TestClassWithExistingMetadata.prototype, 'testMethod', 1); 43 | 44 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClassWithExistingMetadata.prototype, 'testMethod'); 45 | expect(metadata).toHaveLength(2); 46 | expect(metadata[1]).toEqual({ 47 | type: ProcedureParamDecoratorType.Options, 48 | index: 1, 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/raw-input.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { RawInput } from '../raw-input.decorator'; 3 | import { PROCEDURE_PARAM_METADATA_KEY } from '../../trpc.constants'; 4 | import { ProcedureParamDecoratorType } from '../../interfaces/factory.interface'; 5 | 6 | describe('RawInput Decorator', () => { 7 | class TestClass { 8 | testMethod(@RawInput() param1: any, @RawInput() param2: any) {} 9 | } 10 | 11 | it('should add metadata to the method', () => { 12 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 13 | expect(metadata).toBeDefined(); 14 | expect(Array.isArray(metadata)).toBe(true); 15 | }); 16 | 17 | it('should add correct metadata for each parameter', () => { 18 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 19 | expect(metadata).toHaveLength(2); 20 | 21 | expect(metadata[0]).toEqual({ 22 | type: ProcedureParamDecoratorType.RawInput, 23 | index: 1, 24 | }); 25 | 26 | expect(metadata[1]).toEqual({ 27 | type: ProcedureParamDecoratorType.RawInput, 28 | index: 0, 29 | }); 30 | }); 31 | 32 | it('should append to existing metadata', () => { 33 | class TestClassWithExistingMetadata { 34 | testMethod(@RawInput() param1: any) {} 35 | } 36 | 37 | // Simulate existing metadata 38 | const existingMetadata = [{ type: 'SomeOtherDecorator', index: 0 }]; 39 | Reflect.defineMetadata(PROCEDURE_PARAM_METADATA_KEY, existingMetadata, TestClassWithExistingMetadata.prototype, 'testMethod'); 40 | 41 | // Apply our decorator 42 | RawInput()(TestClassWithExistingMetadata.prototype, 'testMethod', 1); 43 | 44 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClassWithExistingMetadata.prototype, 'testMethod'); 45 | expect(metadata).toHaveLength(2); 46 | expect(metadata[1]).toEqual({ 47 | type: ProcedureParamDecoratorType.RawInput, 48 | index: 1, 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/module-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { RootConfigTypes } from '@trpc/server/dist/core/internals/config'; 2 | import { ErrorFormatter } from '@trpc/server/dist/error/formatter'; 3 | import { TRPCErrorShape } from '@trpc/server/dist/rpc'; 4 | import { TRPCContext } from './context.interface'; 5 | import type { Class } from 'type-fest'; 6 | import { ZodTypeAny } from 'zod'; 7 | 8 | export type SchemaImports = 9 | | ((...args: Array) => unknown) 10 | | object 11 | | ZodTypeAny; 12 | 13 | /** 14 | * "TRPCModule" options object. 15 | */ 16 | export interface TRPCModuleOptions { 17 | /** 18 | * Path to trpc app router and helpers types output. 19 | */ 20 | autoSchemaFile?: string; 21 | 22 | /** 23 | * Specifies additional imports for the schema file. This array can include functions, objects, or Zod schemas. 24 | * While `nestjs-trpc` typically handles imports automatically, this option allows manual inclusion of imports for exceptional cases. 25 | * Use this property only when automatic import resolution is insufficient. 26 | * 27 | * Please consider opening an issue on Github so we can update the adapter to better handle your case. 28 | */ 29 | schemaFileImports?: Array; 30 | 31 | /** 32 | * The base path for all trpc requests. 33 | * @default "/trpc" 34 | */ 35 | basePath?: string; 36 | 37 | /** 38 | * The exposed trpc options when creating a route with either `createExpressMiddleware` or `createFastifyMiddleware`. 39 | * If not provided, the adapter will use a default createContext. 40 | * @link https://nestjs-trpc.io/docs/context 41 | */ 42 | context?: Class; 43 | 44 | /** 45 | * Use custom error formatting 46 | * @link https://trpc.io/docs/error-formatting 47 | */ 48 | errorFormatter?: ErrorFormatter< 49 | RootConfigTypes['ctx'], 50 | TRPCErrorShape & { [key: string]: any } 51 | >; 52 | 53 | /** 54 | * Use a data transformer 55 | * @link https://trpc.io/docs/data-transformers 56 | */ 57 | transformer?: RootConfigTypes['transformer']; 58 | } 59 | -------------------------------------------------------------------------------- /docs/public/sitemap-0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://nestjs-trpc.io2024-08-25T22:48:16.380Zdaily0.7 4 | https://nestjs-trpc.io/docs2024-08-25T22:48:16.380Zdaily0.7 5 | https://nestjs-trpc.io/docs/client2024-08-25T22:48:16.380Zdaily0.7 6 | https://nestjs-trpc.io/docs/context2024-08-25T22:48:16.380Zdaily0.7 7 | https://nestjs-trpc.io/docs/dependency-injection2024-08-25T22:48:16.380Zdaily0.7 8 | https://nestjs-trpc.io/docs/graphql2024-08-25T22:48:16.380Zdaily0.7 9 | https://nestjs-trpc.io/docs/middlewares2024-08-25T22:48:16.380Zdaily0.7 10 | https://nestjs-trpc.io/docs/nestjs2024-08-25T22:48:16.380Zdaily0.7 11 | https://nestjs-trpc.io/docs/routers2024-08-25T22:48:16.380Zdaily0.7 12 | https://nestjs-trpc.io/docs/structure2024-08-25T22:48:16.380Zdaily0.7 13 | https://nestjs-trpc.io/docs/trpc2024-08-25T22:48:16.380Zdaily0.7 14 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/input.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Input } from '../input.decorator'; 3 | import { PROCEDURE_PARAM_METADATA_KEY } from '../../trpc.constants'; 4 | import { ProcedureParamDecoratorType } from '../../interfaces/factory.interface'; 5 | 6 | describe('Input Decorator', () => { 7 | class TestClass { 8 | testMethod(@Input() param1: any, @Input('key') param2: any) {} 9 | } 10 | 11 | it('should add metadata to the method', () => { 12 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 13 | expect(metadata).toBeDefined(); 14 | expect(Array.isArray(metadata)).toBe(true); 15 | }); 16 | 17 | it('should add correct metadata for each parameter', () => { 18 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 19 | expect(metadata).toHaveLength(2); 20 | 21 | expect(metadata[0]).toEqual({ 22 | type: ProcedureParamDecoratorType.Input, 23 | index: 1, 24 | key: 'key', 25 | }); 26 | 27 | expect(metadata[1]).toEqual({ 28 | type: ProcedureParamDecoratorType.Input, 29 | index: 0, 30 | key: undefined, 31 | }); 32 | }); 33 | 34 | it('should append to existing metadata', () => { 35 | class TestClassWithExistingMetadata { 36 | testMethod(@Input() param1: any) {} 37 | } 38 | 39 | // Simulate existing metadata 40 | const existingMetadata = [{ type: 'SomeOtherDecorator', index: 0 }]; 41 | Reflect.defineMetadata(PROCEDURE_PARAM_METADATA_KEY, existingMetadata, TestClassWithExistingMetadata.prototype, 'testMethod'); 42 | 43 | // Apply our decorator 44 | Input('newKey')(TestClassWithExistingMetadata.prototype, 'testMethod', 1); 45 | 46 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClassWithExistingMetadata.prototype, 'testMethod'); 47 | expect(metadata).toHaveLength(2); 48 | expect(metadata[1]).toEqual({ 49 | type: ProcedureParamDecoratorType.Input, 50 | index: 1, 51 | key: 'newKey', 52 | }); 53 | }); 54 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/context.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Ctx } from '../context.decorator'; 3 | import { PROCEDURE_PARAM_METADATA_KEY } from '../../trpc.constants'; 4 | import { ProcedureParamDecoratorType } from '../../interfaces/factory.interface'; 5 | 6 | describe('Context Decorator', () => { 7 | class TestClass { 8 | testMethod(@Ctx() param1: any, @Ctx() param2: any) {} 9 | } 10 | 11 | it('should add metadata to the method parameters', () => { 12 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 13 | expect(metadata).toBeDefined(); 14 | expect(Array.isArray(metadata)).toBe(true); 15 | }); 16 | 17 | it('should add correct metadata for each parameter', () => { 18 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testMethod'); 19 | expect(metadata).toHaveLength(2); 20 | 21 | expect(metadata[0]).toEqual({ 22 | type: ProcedureParamDecoratorType.Ctx, 23 | index: 1, 24 | }); 25 | 26 | expect(metadata[1]).toEqual({ 27 | type: ProcedureParamDecoratorType.Ctx, 28 | index: 0, 29 | }); 30 | }); 31 | 32 | it('should not add metadata for properties', () => { 33 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClass.prototype, 'testProperty'); 34 | expect(metadata).toBeUndefined(); 35 | }); 36 | 37 | it('should append to existing metadata', () => { 38 | class TestClassWithExistingMetadata { 39 | testMethod(@Ctx() param1: any) {} 40 | } 41 | 42 | // Simulate existing metadata 43 | const existingMetadata = [{ type: 'SomeOtherDecorator', index: 0 }]; 44 | Reflect.defineMetadata(PROCEDURE_PARAM_METADATA_KEY, existingMetadata, TestClassWithExistingMetadata.prototype, 'testMethod'); 45 | 46 | // Apply our decorator 47 | Ctx()(TestClassWithExistingMetadata.prototype, 'testMethod', 1); 48 | 49 | const metadata = Reflect.getMetadata(PROCEDURE_PARAM_METADATA_KEY, TestClassWithExistingMetadata.prototype, 'testMethod'); 50 | expect(metadata).toHaveLength(2); 51 | expect(metadata[1]).toEqual({ 52 | type: ProcedureParamDecoratorType.Ctx, 53 | index: 1, 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/query.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Query } from '../query.decorator'; 3 | import { PROCEDURE_METADATA_KEY, PROCEDURE_TYPE_KEY } from '../../trpc.constants'; 4 | import { ProcedureType } from '../../trpc.enum'; 5 | import { z } from 'zod'; 6 | 7 | describe('Query Decorator', () => { 8 | it('should set procedure type metadata', () => { 9 | class TestClass { 10 | @Query() 11 | testMethod() {} 12 | } 13 | 14 | const metadata = Reflect.getMetadata(PROCEDURE_TYPE_KEY, TestClass.prototype.testMethod); 15 | expect(metadata).toBe(ProcedureType.Query); 16 | }); 17 | 18 | it('should set procedure metadata with input and output schemas', () => { 19 | const inputSchema = z.string(); 20 | const outputSchema = z.number(); 21 | 22 | class TestClass { 23 | @Query({ input: inputSchema, output: outputSchema }) 24 | testMethod() {} 25 | } 26 | 27 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 28 | expect(metadata).toEqual({ input: inputSchema, output: outputSchema }); 29 | }); 30 | 31 | it('should set procedure metadata without schemas', () => { 32 | class TestClass { 33 | @Query() 34 | testMethod() {} 35 | } 36 | 37 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 38 | expect(metadata).toBeUndefined(); 39 | }); 40 | 41 | it('should set procedure metadata with only input schema', () => { 42 | const inputSchema = z.string(); 43 | 44 | class TestClass { 45 | @Query({ input: inputSchema }) 46 | testMethod() {} 47 | } 48 | 49 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 50 | expect(metadata).toEqual({ input: inputSchema }); 51 | }); 52 | 53 | it('should set procedure metadata with only output schema', () => { 54 | const outputSchema = z.number(); 55 | 56 | class TestClass { 57 | @Query({ output: outputSchema }) 58 | testMethod() {} 59 | } 60 | 61 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 62 | expect(metadata).toEqual({ output: outputSchema }); 63 | }); 64 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/interfaces/factory.interface.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ProcedureRouterRecord, 3 | AnyRouter, 4 | ProcedureBuilder, 5 | ProcedureType, 6 | ProcedureParams, 7 | } from '@trpc/server'; 8 | import type { ZodSchema, ZodType, ZodTypeDef } from 'zod'; 9 | import type { TRPCMiddleware } from './middleware.interface'; 10 | import type { Class, Constructor } from 'type-fest'; 11 | 12 | export enum ProcedureParamDecoratorType { 13 | Options = 'options', 14 | Ctx = 'ctx', 15 | Input = 'input', 16 | RawInput = 'rawInput', 17 | Type = 'type', 18 | Path = 'path', 19 | } 20 | 21 | export type ProcedureImplementation = ({ 22 | input, 23 | output, 24 | }: { 25 | input?: ZodType; 26 | output?: ZodType; 27 | }) => any; 28 | 29 | interface ProcedureParamDecoratorBase { 30 | type: ProcedureParamDecoratorType; 31 | index: number; 32 | } 33 | 34 | export type ProcedureInputParamDecorator = ProcedureParamDecoratorBase & { 35 | type: ProcedureParamDecoratorType.Input; 36 | key?: string; 37 | }; 38 | 39 | export type ProcedureParamDecorator = 40 | | ProcedureParamDecoratorBase 41 | | ProcedureInputParamDecorator; 42 | 43 | export interface ProcedureFactoryMetadata { 44 | type: ProcedureType; 45 | input: ZodSchema | undefined; 46 | output: ZodSchema | undefined; 47 | middlewares: Array | Class>; 48 | name: string; 49 | implementation: ProcedureImplementation; 50 | params: Array | undefined; 51 | } 52 | 53 | export interface CustomProcedureFactoryMetadata { 54 | name: string; 55 | instance: unknown; 56 | } 57 | 58 | export interface RouterInstance { 59 | name: string; 60 | path: string; 61 | instance: unknown; 62 | middlewares: Array | Constructor>; 63 | alias?: string; 64 | } 65 | 66 | export interface RoutersFactoryMetadata { 67 | name: string; 68 | path: string; 69 | alias?: string; 70 | instance: RouterInstance; 71 | procedures: Array; 72 | } 73 | 74 | export type TRPCRouter = ( 75 | procedures: TProcRouterRecord, 76 | ) => AnyRouter; 77 | 78 | export type TRPCPublicProcedure = ProcedureBuilder; 79 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/__tests__/mutation.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Mutation } from '../mutation.decorator'; 3 | import { PROCEDURE_METADATA_KEY, PROCEDURE_TYPE_KEY } from '../../trpc.constants'; 4 | import { ProcedureType } from '../../trpc.enum'; 5 | import { ZodSchema, z } from 'zod'; 6 | 7 | describe('Mutation Decorator', () => { 8 | it('should set procedure type metadata', () => { 9 | class TestClass { 10 | @Mutation() 11 | testMethod() {} 12 | } 13 | 14 | const metadata = Reflect.getMetadata(PROCEDURE_TYPE_KEY, TestClass.prototype.testMethod); 15 | expect(metadata).toBe(ProcedureType.Mutation); 16 | }); 17 | 18 | it('should set procedure metadata with input and output schemas', () => { 19 | const inputSchema: ZodSchema = z.string(); 20 | const outputSchema: ZodSchema = z.number(); 21 | 22 | class TestClass { 23 | @Mutation({ input: inputSchema, output: outputSchema }) 24 | testMethod() {} 25 | } 26 | 27 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 28 | expect(metadata).toEqual({ input: inputSchema, output: outputSchema }); 29 | }); 30 | 31 | it('should set procedure metadata without schemas', () => { 32 | class TestClass { 33 | @Mutation() 34 | testMethod() {} 35 | } 36 | 37 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 38 | expect(metadata).toBeUndefined(); 39 | }); 40 | 41 | it('should set procedure metadata with only input schema', () => { 42 | const inputSchema: ZodSchema = z.string(); 43 | 44 | class TestClass { 45 | @Mutation({ input: inputSchema }) 46 | testMethod() {} 47 | } 48 | 49 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 50 | expect(metadata).toEqual({ input: inputSchema }); 51 | }); 52 | 53 | it('should set procedure metadata with only output schema', () => { 54 | const outputSchema: ZodSchema = z.number(); 55 | 56 | class TestClass { 57 | @Mutation({ output: outputSchema }) 58 | testMethod() {} 59 | } 60 | 61 | const metadata = Reflect.getMetadata(PROCEDURE_METADATA_KEY, TestClass.prototype.testMethod); 62 | expect(metadata).toEqual({ output: outputSchema }); 63 | }); 64 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-trpc", 3 | "version": "1.6.1", 4 | "homepage": "https://nestjs-trpc.io", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "dist" 10 | ], 11 | "exports": { 12 | ".": { 13 | "import": "./dist/index.js", 14 | "require": "./dist/index.js", 15 | "types": "./dist/index.d.ts" 16 | } 17 | }, 18 | "scripts": { 19 | "build": "rm -rf dist && tsc --project tsconfig.build.json", 20 | "start:dev": "tsc --project tsconfig.json --watch --preserveWatchOutput", 21 | "debug:dev": "ts-node --inspect-brk lib/index.ts", 22 | "clean": "tsc -b --clean", 23 | "test": "TS_JEST_DISABLE_VER_CHECKER=true jest --no-watchman --coverage" 24 | }, 25 | "keywords": [ 26 | "nestjs", 27 | "trpc", 28 | "adapter", 29 | "nest", 30 | "express", 31 | "fastify", 32 | "api", 33 | "typescript", 34 | "types" 35 | ], 36 | "author": { 37 | "name": "Kevin Edry", 38 | "email": "kevin.edry@gmail.com", 39 | "url": "https://kevin-edry.com" 40 | }, 41 | "publishConfig": { 42 | "access": "public", 43 | "registry": "https://registry.npmjs.org" 44 | }, 45 | "peerDependencies": { 46 | "@nestjs/common": "^9.3.8 || ^10.0.0", 47 | "@nestjs/core": "^9.3.8 || ^10.0.0", 48 | "@trpc/server": "^10.0.0", 49 | "reflect-metadata": "^0.1.13 || ^0.2.0", 50 | "rxjs": "7.8.1", 51 | "zod": "^3.14.0" 52 | }, 53 | "devDependencies": { 54 | "@nestjs/common": "10.4.1", 55 | "@nestjs/core": "10.4.1", 56 | "@nestjs/testing": "10.4.1", 57 | "@trpc/server": "^10.18.0", 58 | "@types/express": "^4.17.17", 59 | "@types/jest": "^29.5.12", 60 | "@types/lodash": "^4.17.5", 61 | "@types/node": "^22.5.0", 62 | "fastify": "^5.0.0", 63 | "jest": "^29.7.0", 64 | "reflect-metadata": "0.1.13", 65 | "rxjs": "7.8.1", 66 | "ts-jest": "^29.2.5", 67 | "ts-node": "10.9.2", 68 | "tsconfig-paths": "^4.2.0", 69 | "type-fest": "^4.21.0", 70 | "typescript": "5.5.3", 71 | "zod": "^3.14.4" 72 | }, 73 | "dependencies": { 74 | "lodash": "^4.17.21", 75 | "ts-morph": "22.0.0", 76 | "tslib": "^2.5.0" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "https://github.com/KevinEdry/nestjs-trpc", 81 | "directory": "packages/nestjs-trpc" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/__tests__/context.generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ContextGenerator } from '../context.generator'; 3 | import { Project, SourceFile } from 'ts-morph'; 4 | import { TRPCContext } from '../../interfaces'; 5 | 6 | describe('ContextGenerator', () => { 7 | let contextGenerator: ContextGenerator; 8 | let project: Project; 9 | let sourceFile: SourceFile; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [ContextGenerator], 14 | }).compile(); 15 | 16 | contextGenerator = module.get(ContextGenerator); 17 | project = new Project(); 18 | 19 | sourceFile = project.createSourceFile( 20 | "test.ts", 21 | ` 22 | import { TRPCContext } from './interfaces'; 23 | 24 | export class TestContext implements TRPCContext { 25 | create() { 26 | return { user: { id: '1', name: 'Test' } }; 27 | } 28 | } 29 | `, { overwrite: true } 30 | ); 31 | }); 32 | 33 | it('should be defined', () => { 34 | expect(contextGenerator).toBeDefined(); 35 | }); 36 | 37 | describe('getContextInterface', () => { 38 | 39 | it('should return the context interface if everything is valid', async () => { 40 | class TestContext implements TRPCContext { 41 | create() { 42 | return { user: { id: '1', name: 'Test' } }; 43 | } 44 | } 45 | 46 | jest.spyOn(project, 'addSourceFileAtPath').mockReturnValue(sourceFile); 47 | 48 | const result = await contextGenerator.getContextInterface(sourceFile, TestContext); 49 | expect(result).toBe('{ user: { id: string; name: string; }; }'); 50 | }); 51 | 52 | it('should return null if create method is not found', async () => { 53 | sourceFile = project.createSourceFile( 54 | "test.ts", 55 | ` 56 | export class InvalidContext { 57 | // No create method 58 | } 59 | `, { overwrite: true } 60 | ); 61 | 62 | class InvalidContext {} 63 | 64 | jest.spyOn(project, 'addSourceFileAtPath').mockReturnValue(sourceFile); 65 | 66 | //@ts-expect-error invalid context passed in 67 | const result = await contextGenerator.getContextInterface(sourceFile, InvalidContext); 68 | expect(result).toBeNull(); 69 | }); 70 | }); 71 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/__tests__/middleware.generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MiddlewareGenerator } from '../middleware.generator'; 3 | import { Project, SourceFile } from 'ts-morph'; 4 | import { TRPCMiddleware } from '../../interfaces'; 5 | 6 | describe('MiddlewareGenerator', () => { 7 | let middlewareGenerator: MiddlewareGenerator; 8 | let project: Project; 9 | let sourceFile: SourceFile; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [MiddlewareGenerator], 14 | }).compile(); 15 | 16 | middlewareGenerator = module.get(MiddlewareGenerator); 17 | project = new Project(); 18 | 19 | sourceFile = project.createSourceFile( 20 | "test.ts", 21 | ` 22 | import { TRPCMiddleware } from './interfaces'; 23 | 24 | export class TestMiddleware implements TRPCMiddleware { 25 | use(opts: any) { 26 | return opts.next({ 27 | ctx: { 28 | user: { id: '1', name: 'Test' }, 29 | }, 30 | }); 31 | } 32 | } 33 | `, 34 | { overwrite: true } 35 | ); 36 | }); 37 | 38 | it('should be defined', () => { 39 | expect(middlewareGenerator).toBeDefined(); 40 | }); 41 | 42 | describe('getMiddlewareInterface', () => { 43 | it('should return null if middleware class name is not defined', async () => { 44 | const result = await middlewareGenerator.getMiddlewareInterface('routerPath', {} as any, project); 45 | expect(result).toBeNull(); 46 | }); 47 | 48 | it('should return the middleware interface if everything is valid', async () => { 49 | class TestMiddleware implements TRPCMiddleware { 50 | use(opts: any) { 51 | return opts.next({ 52 | ctx: { 53 | user: { id: '1', name: 'Test' }, 54 | }, 55 | }); 56 | } 57 | } 58 | 59 | jest.spyOn(project, 'addSourceFileAtPath').mockReturnValue(sourceFile); 60 | 61 | const result = await middlewareGenerator.getMiddlewareInterface('routerPath', TestMiddleware, project); 62 | expect(result).toEqual({ 63 | name: 'TestMiddleware', 64 | properties: [ 65 | { name: 'user', type: '{ id: string; name: string; }' }, 66 | ], 67 | }); 68 | }); 69 | }); 70 | }); -------------------------------------------------------------------------------- /docs/pages/docs/graphql.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC Documentation - GraphQL" 3 | --- 4 | 5 | import { Callout} from 'nextra/components'; 6 | 7 | # Why Migrate from GraphQL? 8 | 9 | The **NestJS tRPC** adapter offers a powerful alternative to NestJS & GraphQL. Here's why you might consider migrating: 10 | 11 | ### Key Advantages 12 | 13 | 1. **End-to-End Type Safety** 14 | Achieve seamless type inference from backend to frontend without code generation, significantly reducing runtime errors caused by type mismatches. 15 | 16 | 2. **Simplified Development** 17 | Eliminate the need for schema definitions and resolvers. Directly utilize your existing models and business logic, resulting in less boilerplate and faster development cycles. 18 | 19 | 3. **Familiar NestJS Patterns** 20 | Leverage your existing NestJS knowledge with minimal learning curve, as `nestjs-trpc` integrates smoothly with the NestJS ecosystem. 21 | 22 | 4. **Performance Benefits** 23 | Enjoy a lighter-weight solution compared to a full GraphQL implementation, with reduced overhead in request processing and response serialization. 24 | 25 | 5. **Seamless Integration** 26 | Integrate tRPC alongside your existing NestJS modules and services, enabling a gradual migration path from GraphQL to tRPC. 27 | 28 | 6. **Enhanced Developer Experience** 29 | Benefit from full IDE support including autocomplete and IntelliSense, easier debugging, and a simplified testing setup compared to GraphQL. 30 | 31 | 7. **Flexible Querying** 32 | Utilize powerful query composition, including the ability to batch multiple procedure calls in a single HTTP request and support for efficient caching and invalidation strategies. 33 | 34 | ### Migration Considerations 35 | 36 | - Learning Curve: Your team will need to familiarize themselves with tRPC concepts, though they are generally straightforward for TypeScript developers. 37 | - Client-Side Updates: Existing client-side code will require updates to use the tRPC client instead of GraphQL queries. 38 | - Feature Parity: Evaluate whether tRPC can fulfill all your current GraphQL use cases before committing to migration. 39 | 40 | 41 | If your application heavily relies on GraphQL-specific features like subscriptions, carefully assess if tRPC can meet these needs before proceeding with migration. 42 | 43 | 44 | For a comprehensive overview of tRPC's capabilities, consult the [official tRPC documentation](https://trpc.io/docs). -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/factories/__tests__/middleware.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MiddlewareFactory } from '../middleware.factory'; 3 | import { RouterFactory } from '../router.factory'; 4 | import { ProcedureFactory } from '../procedure.factory'; 5 | 6 | describe('MiddlewareFactory', () => { 7 | let middlewareFactory: MiddlewareFactory; 8 | let routerFactory: RouterFactory; 9 | let procedureFactory: ProcedureFactory; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [ 14 | MiddlewareFactory, 15 | { 16 | provide: RouterFactory, 17 | useValue: { 18 | getRouters: jest.fn(), 19 | }, 20 | }, 21 | { 22 | provide: ProcedureFactory, 23 | useValue: { 24 | getProcedures: jest.fn(), 25 | }, 26 | }, 27 | ], 28 | }).compile(); 29 | 30 | middlewareFactory = module.get(MiddlewareFactory); 31 | routerFactory = module.get(RouterFactory); 32 | procedureFactory = module.get(ProcedureFactory); 33 | }); 34 | 35 | it('should be defined', () => { 36 | expect(middlewareFactory).toBeDefined(); 37 | }); 38 | 39 | describe('getMiddlewares', () => { 40 | it('should return unique middlewares', () => { 41 | const mockRouter = { instance: {}, prototype: {}, middlewares: [] }; 42 | const mockProcedure = { middlewares: [class TestMiddleware {}] }; 43 | 44 | (routerFactory.getRouters as jest.Mock).mockReturnValue([mockRouter]); 45 | (procedureFactory.getProcedures as jest.Mock).mockReturnValue([mockProcedure]); 46 | 47 | const result = middlewareFactory.getMiddlewares(); 48 | 49 | expect(result).toHaveLength(1); 50 | expect(result[0]).toStrictEqual({"instance": mockProcedure.middlewares[0], "path": undefined }); 51 | }); 52 | 53 | it('should handle procedures without middlewares', () => { 54 | const mockRouter = { instance: {}, prototype: {}, middlewares: [] }; 55 | const mockProcedure = { middlewares: undefined }; 56 | 57 | (routerFactory.getRouters as jest.Mock).mockReturnValue([mockRouter]); 58 | (procedureFactory.getProcedures as jest.Mock).mockReturnValue([mockProcedure]); 59 | 60 | const result = middlewareFactory.getMiddlewares(); 61 | 62 | expect(result).toHaveLength(0); 63 | }); 64 | }); 65 | }); -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Project } from 'ts-morph'; 3 | import { 4 | ProcedureGeneratorMetadata, 5 | } from '../../interfaces/generator.interface'; 6 | import { ProcedureGenerator } from '../procedure.generator'; 7 | import { ImportsScanner } from '../../scanners/imports.scanner'; 8 | import { StaticGenerator } from '../static.generator'; 9 | import { TYPESCRIPT_APP_ROUTER_SOURCE_FILE } from '../generator.constants'; 10 | 11 | describe('ProcedureGenerator', () => { 12 | let procedureGenerator: ProcedureGenerator; 13 | let project: Project; 14 | 15 | beforeEach(async () => { 16 | const module: TestingModule = await Test.createTestingModule({ 17 | providers: [ 18 | ProcedureGenerator, 19 | { 20 | provide: ImportsScanner, 21 | useValue: jest.fn(), 22 | }, 23 | { 24 | provide: StaticGenerator, 25 | useValue: jest.fn(), 26 | }, 27 | { 28 | provide: TYPESCRIPT_APP_ROUTER_SOURCE_FILE, 29 | useValue: jest.fn(), 30 | }, 31 | ], 32 | }).compile(); 33 | 34 | procedureGenerator = module.get(ProcedureGenerator); 35 | }); 36 | 37 | it('should be defined', () => { 38 | expect(procedureGenerator).toBeDefined(); 39 | }); 40 | 41 | describe('generateRoutersStringFromMetadata', () => { 42 | describe('for a query', () => { 43 | it('should generate router string from metadata', () => { 44 | const mockProcedure: ProcedureGeneratorMetadata = { 45 | name: 'testQuery', 46 | decorators: [{ name: 'Query', arguments: {} }], 47 | } 48 | 49 | const result = procedureGenerator.generateProcedureString(mockProcedure); 50 | 51 | expect(result).toBe( 52 | 'testQuery: publicProcedure.query(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any )' 53 | ); 54 | }); 55 | }) 56 | 57 | describe('for a mutation', () => { 58 | it('should generate router string from metadata', () => { 59 | const mockProcedure: ProcedureGeneratorMetadata = { 60 | name: 'testMutation', 61 | decorators: [{ name: 'Mutation', arguments: {} }], 62 | } 63 | 64 | const result = procedureGenerator.generateProcedureString(mockProcedure); 65 | 66 | expect(result).toBe( 67 | 'testMutation: publicProcedure.mutation(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any )' 68 | ); 69 | }); 70 | }) 71 | }); 72 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-trpc-workspace", 3 | "version": "1.6.1", 4 | "homepage": "https://nestjs-trpc.io", 5 | "private": true, 6 | "scripts": { 7 | "build": "tsc -b -v packages", 8 | "changelog": "lerna-changelog", 9 | "clean": "tsc -b --clean", 10 | "format": "prettier packages/**/*.ts --ignore-path ./.prettierignore --write", 11 | "lint": "eslint 'packages/**/*.ts' --fix", 12 | "test": "yarn workspaces foreach --all run test", 13 | "release": "release-it", 14 | "prepublish:npm": "yarn build && yarn changelog | pbcopy", 15 | "publish:npm": "lerna publish", 16 | "prepublish:next": "yarn build", 17 | "publish:next": "lerna publish --dist-tag next", 18 | "prepack": "pinst --disable", 19 | "postinstall": "husky", 20 | "postpack": "pinst --enable", 21 | "prepare": "husky" 22 | }, 23 | "author": { 24 | "name": "Kevin Edry", 25 | "email": "kevin.edry@gmail.com", 26 | "url": "https://kevin-edry.com" 27 | }, 28 | "license": "MIT", 29 | "engines": { 30 | "node": "18 || 19 || 20" 31 | }, 32 | "workspaces": [ 33 | "packages/*", 34 | "examples/*", 35 | "docs" 36 | ], 37 | "devDependencies": { 38 | "@commitlint/cli": "^19.4.0", 39 | "@commitlint/config-conventional": "^19.2.2", 40 | "@commitlint/prompt-cli": "^19.3.1", 41 | "@eslint/eslintrc": "^3.1.0", 42 | "@eslint/js": "^9.11.1", 43 | "@nestjs/cli": "^9.3.0", 44 | "@release-it-plugins/workspaces": "^4.2.0", 45 | "@release-it/conventional-changelog": "^8.0.1", 46 | "@types/jest": "29.5.12", 47 | "@types/node": "20.14.11", 48 | "@typescript-eslint/eslint-plugin": "^8.2.0", 49 | "@typescript-eslint/parser": "^8.2.0", 50 | "conventional-changelog-conventionalcommits": "^8.0.0", 51 | "eslint": "9.7.0", 52 | "eslint-config-prettier": "9.1.0", 53 | "eslint-plugin-import": "^2.29.1", 54 | "eslint-plugin-prettier": "5.2.1", 55 | "globals": "^15.9.0", 56 | "husky": "^9.1.5", 57 | "jest": "^29.7.0", 58 | "lint-staged": "15.2.7", 59 | "pinst": "^3.0.0", 60 | "prettier": "^3.3.3", 61 | "release-it": "17.1.1", 62 | "rimraf": "^4.4.1", 63 | "ts-jest": "29.2.3", 64 | "tsconfig-paths": "^4.2.0", 65 | "typescript": "^5.5.4" 66 | }, 67 | "lint-staged": { 68 | "*.ts": [ 69 | "prettier --write", 70 | "eslint --fix", 71 | "yarn test --bail --findRelatedTests" 72 | ] 73 | }, 74 | "repository": { 75 | "type": "git", 76 | "url": "https://github.com/KevinEdry/nestjs-trpc" 77 | }, 78 | "packageManager": "yarn@4.4.0" 79 | } 80 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/static.generator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImportDeclarationStructure, 3 | SourceFile, 4 | StructureKind, 5 | Type, 6 | VariableDeclarationKind, 7 | } from 'ts-morph'; 8 | import { Injectable } from '@nestjs/common'; 9 | import { SourceFileImportsMap } from '../interfaces/generator.interface'; 10 | import * as path from 'node:path'; 11 | 12 | @Injectable() 13 | export class StaticGenerator { 14 | public generateStaticDeclaration(sourceFile: SourceFile): void { 15 | sourceFile.addImportDeclaration({ 16 | kind: StructureKind.ImportDeclaration, 17 | moduleSpecifier: '@trpc/server', 18 | namedImports: ['initTRPC'], 19 | }); 20 | sourceFile.addImportDeclaration({ 21 | kind: StructureKind.ImportDeclaration, 22 | moduleSpecifier: 'zod', 23 | namedImports: ['z'], 24 | }); 25 | 26 | sourceFile.addVariableStatements([ 27 | { 28 | declarationKind: VariableDeclarationKind.Const, 29 | declarations: [{ name: 't', initializer: 'initTRPC.create()' }], 30 | }, 31 | { 32 | declarationKind: VariableDeclarationKind.Const, 33 | declarations: [{ name: 'publicProcedure', initializer: 't.procedure' }], 34 | }, 35 | ]); 36 | } 37 | 38 | public addSchemaImports( 39 | sourceFile: SourceFile, 40 | schemaImportNames: Array, 41 | importsMap: Map, 42 | ): void { 43 | const importDeclarations: ImportDeclarationStructure[] = []; 44 | 45 | for (const schemaImportName of schemaImportNames) { 46 | for (const [importMapKey, importMapMetadata] of importsMap.entries()) { 47 | if (schemaImportName == null || importMapKey !== schemaImportName) { 48 | continue; 49 | } 50 | 51 | const relativePath = path.relative( 52 | path.dirname(sourceFile.getFilePath()), 53 | importMapMetadata.sourceFile.getFilePath().replace(/\.ts$/, ''), 54 | ); 55 | 56 | importDeclarations.push({ 57 | kind: StructureKind.ImportDeclaration, 58 | moduleSpecifier: relativePath.startsWith('.') 59 | ? relativePath 60 | : `./${relativePath}`, 61 | namedImports: [schemaImportName], 62 | }); 63 | } 64 | } 65 | 66 | sourceFile.addImportDeclarations(importDeclarations); 67 | } 68 | 69 | public findCtxOutProperty(type: Type): string | undefined { 70 | const typeText = type.getText(); 71 | const ctxOutMatch = typeText.match(/_ctx_out:\s*{([^}]*)}/); 72 | 73 | return ctxOutMatch ? ctxOutMatch[1].trim() : undefined; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/pages/docs/structure.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC Documentation - First Steps and Core Concepts" 3 | --- 4 | 5 | import { Callout, FileTree } from 'nextra/components'; 6 | import Link from 'next/link'; 7 | import Table from '../../components/Table'; 8 | 9 | # First Steps and Core Concepts 10 | 11 | In this documentation we will go through the basic concepts and recommended file structure of this adapter, 12 | our main goal developing this adapter was to make it synonymous with NestJS and it's opinionated approach and concepts. 13 | 14 | If you are looking for an installation guide for this adapter, check out our Setup Guide. 15 | 16 | ### Basic File Structure 17 | In **NestJS tRPC** we take a similar approach to how NestJS implements it's routing system, meaning the file structure should remain similar. 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Here's a brief overview of those core files: 31 | 32 | 33 | 34 | `app.router.ts` 35 | Comparable to a NestJS controller, a basic router with a single route. 36 | 37 | 38 | `app.middleware.ts` 39 | Comparable to a NestJS guards or middlewares, its a middleware that can modifythe request ctx. 40 | 41 | 42 | `app.context.ts` 43 | A context class that modifies the application context. 44 | 45 | 46 | `app.service.ts` 47 | A basic service, The same service you will use in any NestJS application. 48 | 49 | 50 | `app.module.ts` 51 | The root module of the application. 52 | 53 | 54 | `main.ts` 55 | The entry file of the application which uses the core function NestFactory to create a Nest application instance. 56 | 57 | 58 | 59 | ### Platform 60 | As of writing, we have native support for both the `express` and the `fastify` drivers, the adapter checks which driver you are currently using and adapts accordingly. -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/context.generator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassDeclaration, 3 | MethodDeclaration, 4 | Type, 5 | SyntaxKind, 6 | SourceFile, 7 | } from 'ts-morph'; 8 | import { Injectable } from '@nestjs/common'; 9 | import type { TRPCContext } from '../interfaces'; 10 | import type { Class } from 'type-fest'; 11 | 12 | @Injectable() 13 | export class ContextGenerator { 14 | public async getContextInterface( 15 | sourceFile: SourceFile, 16 | context: Class, 17 | ): Promise { 18 | const className = context?.name; 19 | if (!className) { 20 | return null; 21 | } 22 | 23 | const contextInstance = new context(); 24 | 25 | if (typeof contextInstance.create !== 'function') { 26 | return null; 27 | } 28 | 29 | const classDeclaration = this.getClassDeclaration(sourceFile, context.name); 30 | 31 | if (!classDeclaration) { 32 | return null; 33 | } 34 | 35 | const createMethod = classDeclaration.getMethod('create'); 36 | if (!createMethod) { 37 | return null; 38 | } 39 | 40 | const ctxType = this.extractReturnTypeFromCreateMethod(createMethod); 41 | if (!ctxType) { 42 | return null; 43 | } 44 | 45 | return ctxType.getText(); 46 | } 47 | 48 | private extractReturnTypeFromCreateMethod( 49 | createMethod: MethodDeclaration, 50 | ): Type | null { 51 | const body = createMethod.getBody(); 52 | if (!body) return null; 53 | 54 | // Find the return statement 55 | const returnStatement = body 56 | .getDescendantsOfKind(SyntaxKind.ReturnStatement) 57 | .find((statement) => statement.getExpression() !== undefined); 58 | 59 | if (!returnStatement) return null; 60 | 61 | const returnExpression = returnStatement.getExpression(); 62 | if (!returnExpression) return null; 63 | 64 | // Get the type of the returned expression 65 | const returnType = returnExpression.getType(); 66 | 67 | // Check if the type is a Promise 68 | if (this.isPromiseType(returnType)) { 69 | // Get the type argument of the Promise 70 | const typeArguments = returnType.getTypeArguments(); 71 | return typeArguments.length > 0 ? typeArguments[0] : null; 72 | } 73 | 74 | return returnType; 75 | } 76 | 77 | private isPromiseType(type: Type): boolean { 78 | return ( 79 | type.getSymbol()?.getName() === 'Promise' || 80 | type.getSymbol()?.getName() === '__global.Promise' || 81 | type.getText().startsWith('Promise<') 82 | ); 83 | } 84 | 85 | private getClassDeclaration( 86 | sourceFile: SourceFile, 87 | className: string, 88 | ): ClassDeclaration | undefined { 89 | const classDeclaration = sourceFile.getClass(className); 90 | if (classDeclaration) { 91 | return classDeclaration; 92 | } 93 | return undefined; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/trpc.module.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger, Inject, Module } from '@nestjs/common'; 2 | import { DynamicModule, OnModuleInit } from '@nestjs/common/interfaces'; 3 | import { HttpAdapterHost } from '@nestjs/core'; 4 | 5 | import { LOGGER_CONTEXT, TRPC_MODULE_OPTIONS } from './trpc.constants'; 6 | 7 | import { TRPCModuleOptions } from './interfaces'; 8 | import { TRPCDriver } from './trpc.driver'; 9 | import { AppRouterHost } from './app-router.host'; 10 | import { ExpressDriver, FastifyDriver } from './drivers'; 11 | import { FileScanner } from './scanners/file.scanner'; 12 | import { GeneratorModule } from './generators/generator.module'; 13 | import { FactoryModule } from './factories/factory.module'; 14 | import { ScannerModule } from './scanners/scanner.module'; 15 | 16 | @Module({ 17 | imports: [FactoryModule, ScannerModule], 18 | providers: [ 19 | // NestJS Providers 20 | ConsoleLogger, 21 | 22 | // Drivers 23 | TRPCDriver, 24 | FastifyDriver, 25 | ExpressDriver, 26 | 27 | // Exports 28 | AppRouterHost, 29 | ], 30 | exports: [AppRouterHost], 31 | }) 32 | export class TRPCModule implements OnModuleInit { 33 | @Inject(TRPC_MODULE_OPTIONS) 34 | private readonly options!: TRPCModuleOptions; 35 | 36 | @Inject(ConsoleLogger) 37 | private readonly consoleLogger!: ConsoleLogger; 38 | 39 | @Inject(HttpAdapterHost) 40 | private readonly httpAdapterHost!: HttpAdapterHost; 41 | 42 | @Inject(TRPCDriver) 43 | private readonly trpcDriver!: TRPCDriver; 44 | 45 | @Inject(AppRouterHost) 46 | private readonly appRouterHost!: AppRouterHost; 47 | 48 | static forRoot(options: TRPCModuleOptions = {}): DynamicModule { 49 | const imports: Array = []; 50 | 51 | if (options.autoSchemaFile != null) { 52 | const fileScanner = new FileScanner(); 53 | const callerFilePath = fileScanner.getCallerFilePath(); 54 | imports.push( 55 | GeneratorModule.forRoot({ 56 | outputDirPath: options.autoSchemaFile, 57 | rootModuleFilePath: callerFilePath, 58 | schemaFileImports: options.schemaFileImports, 59 | context: options.context, 60 | }), 61 | ); 62 | } 63 | 64 | return { 65 | module: TRPCModule, 66 | imports, 67 | providers: [{ provide: TRPC_MODULE_OPTIONS, useValue: options }], 68 | }; 69 | } 70 | 71 | async onModuleInit() { 72 | const httpAdapter = this.httpAdapterHost?.httpAdapter; 73 | if (!httpAdapter) { 74 | return; 75 | } 76 | 77 | this.consoleLogger.setContext(LOGGER_CONTEXT); 78 | 79 | await this.trpcDriver.start(this.options); 80 | 81 | const platformName = httpAdapter.getType(); 82 | if (this.appRouterHost.appRouter != null) { 83 | this.consoleLogger.log( 84 | `Server has been initialized successfully using the ${platformName} driver.`, 85 | 'TRPC Server', 86 | ); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/decorator.generator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Decorator, 3 | Expression, 4 | Project, 5 | SourceFile, 6 | SyntaxKind, 7 | } from 'ts-morph'; 8 | import { DecoratorGeneratorMetadata } from '../interfaces/generator.interface'; 9 | import { ConsoleLogger, Inject, Injectable } from '@nestjs/common'; 10 | import { ProcedureGenerator } from './procedure.generator'; 11 | 12 | @Injectable() 13 | export class DecoratorGenerator { 14 | @Inject(ConsoleLogger) 15 | private readonly consoleLogger!: ConsoleLogger; 16 | 17 | @Inject(ProcedureGenerator) 18 | private readonly procedureGenerator!: ProcedureGenerator; 19 | 20 | public serializeProcedureDecorators( 21 | decorators: Decorator[], 22 | sourceFile: SourceFile, 23 | project: Project, 24 | ): Array { 25 | return decorators.reduce( 26 | (array, decorator) => { 27 | const decoratorName = decorator.getName(); 28 | 29 | if (decoratorName === 'Query' || decoratorName === 'Mutation') { 30 | const input = this.getDecoratorPropertyValue( 31 | decorator, 32 | 'input', 33 | sourceFile, 34 | project, 35 | ); 36 | const output = this.getDecoratorPropertyValue( 37 | decorator, 38 | 'output', 39 | sourceFile, 40 | project, 41 | ); 42 | 43 | array.push({ 44 | name: decoratorName, 45 | arguments: { 46 | ...(input ? { input } : {}), 47 | ...(output ? { output } : {}), 48 | }, 49 | }); 50 | } else if ( 51 | decoratorName === 'UseMiddlewares' || 52 | decoratorName === 'Middlewares' 53 | ) { 54 | return array; 55 | } else { 56 | this.consoleLogger.warn(`Decorator ${decoratorName}, not supported.`); 57 | } 58 | 59 | return array; 60 | }, 61 | [], 62 | ); 63 | } 64 | 65 | public getDecoratorPropertyValue( 66 | decorator: Decorator, 67 | propertyName: string, 68 | sourceFile: SourceFile, 69 | project: Project, 70 | ): string | null { 71 | const args = decorator.getArguments(); 72 | 73 | for (const arg of args) { 74 | if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) { 75 | const properties = (arg as any).getProperties(); 76 | const property = properties.find( 77 | (p: any) => p.getName() === propertyName, 78 | ); 79 | 80 | if (!property) { 81 | return null; 82 | } 83 | 84 | const propertyInitializer: Expression = property.getInitializer(); 85 | return this.procedureGenerator.flattenZodSchema( 86 | propertyInitializer, 87 | sourceFile, 88 | project, 89 | propertyInitializer.getText(), 90 | ); 91 | } 92 | } 93 | 94 | return null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/router.generator.ts: -------------------------------------------------------------------------------- 1 | import { Project } from 'ts-morph'; 2 | import { 3 | RouterGeneratorMetadata, 4 | ProcedureGeneratorMetadata, 5 | } from '../interfaces/generator.interface'; 6 | import { 7 | RoutersFactoryMetadata, 8 | ProcedureFactoryMetadata, 9 | } from '../interfaces/factory.interface'; 10 | import { DecoratorGenerator } from './decorator.generator'; 11 | import { Inject, Injectable } from '@nestjs/common'; 12 | import { camelCase } from 'lodash'; 13 | import { ProcedureGenerator } from './procedure.generator'; 14 | 15 | @Injectable() 16 | export class RouterGenerator { 17 | @Inject(DecoratorGenerator) 18 | private readonly decoratorHandler!: DecoratorGenerator; 19 | 20 | @Inject(ProcedureGenerator) 21 | private readonly procedureGenerator!: ProcedureGenerator; 22 | 23 | public serializeRouters( 24 | routers: Array, 25 | project: Project, 26 | ): Array { 27 | return routers.map((router) => { 28 | const proceduresMetadata = router.procedures.map((procedure) => 29 | this.serializeRouterProcedures( 30 | router.path, 31 | procedure, 32 | router.name, 33 | project, 34 | ), 35 | ); 36 | 37 | return { 38 | name: router.name, 39 | alias: router.alias, 40 | procedures: proceduresMetadata, 41 | }; 42 | }); 43 | } 44 | 45 | private serializeRouterProcedures( 46 | routerFilePath: string, 47 | procedure: ProcedureFactoryMetadata, 48 | routerName: string, 49 | project: Project, 50 | ): ProcedureGeneratorMetadata { 51 | const sourceFile = project.addSourceFileAtPath(routerFilePath); 52 | const classDeclaration = sourceFile.getClass(routerName); 53 | 54 | if (!classDeclaration) { 55 | throw new Error(`Could not find router ${routerName} class declaration.`); 56 | } 57 | 58 | const methodDeclaration = classDeclaration.getMethod(procedure.name); 59 | 60 | if (!methodDeclaration) { 61 | throw new Error(`Could not find ${routerName}, method declarations.`); 62 | } 63 | 64 | const decorators = methodDeclaration.getDecorators(); 65 | 66 | if (!decorators) { 67 | throw new Error( 68 | `could not find ${methodDeclaration.getName()}, method decorators.`, 69 | ); 70 | } 71 | 72 | const serializedDecorators = 73 | this.decoratorHandler.serializeProcedureDecorators( 74 | decorators, 75 | sourceFile, 76 | project, 77 | ); 78 | 79 | return { 80 | name: procedure.name, 81 | decorators: serializedDecorators, 82 | }; 83 | } 84 | 85 | public generateRoutersStringFromMetadata( 86 | routers: Array, 87 | ): string { 88 | return routers 89 | .map((router) => { 90 | const { name, procedures, alias } = router; 91 | return `${alias ?? camelCase(name)}: t.router({ ${procedures 92 | .map(this.procedureGenerator.generateProcedureString) 93 | .join(',\n')} })`; 94 | }) 95 | .join(',\n'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /docs/pages/docs/trpc.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC Documentation - tRPC Guide and Concepts" 3 | --- 4 | 5 | # tRPC Core Concepts 6 | 7 | ### What is tRPC? 8 | 9 | [tRPC](https://trpc.io/) is a powerful library for building end-to-end typesafe APIs without the need for a code generator. It allows you to define your APIs using TypeScript, ensuring that your client and server are always in sync. 10 | 11 | ### Why Choose tRPC? 12 | 13 | - **Type Safety**: Ensures end-to-end type safety without the need for code generation. 14 | - **Productivity**: Enhances developer productivity by reducing boilerplate code and runtime errors. 15 | - **Flexibility**: Can be used with any framework, including React, Next.js, and even vanilla TypeScript projects. 16 | - **Lightweight**: Minimal overhead compared to traditional REST or GraphQL. 17 | 18 | ### Core Concepts 19 | 20 | #### Routers and Procedures 21 | 22 | tRPC revolves around the concept of routers and procedures. A router can contain multiple procedures, which are either queries or mutations. 23 | 24 | - **Router**: A collection of related procedures. 25 | - **Procedure**: A function exposed through the router, which can be a query (read-only operation) or a mutation (write operation). 26 | 27 | #### Type Inference 28 | 29 | tRPC leverages TypeScript's type inference to ensure that types are consistent across the client and server. When you define a router and its procedures, tRPC automatically infers the types for you. 30 | 31 | #### Data Transformation 32 | 33 | tRPC allows you to define custom data transformers for serializing and deserializing data, ensuring that your API can handle complex data structures seamlessly. 34 | 35 | #### Middlewares 36 | 37 | tRPC supports middlewares, allowing you to add custom logic to your procedures. This can include authentication, authorization, logging, etc. 38 | 39 | ### Example Concepts 40 | 41 | #### Defining a Router 42 | 43 | Here's how you can define a simple router with a query and a mutation: 44 | 45 | ```typescript 46 | import { initTRPC } from '@trpc/server'; 47 | 48 | const t = initTRPC.create(); 49 | 50 | const appRouter = t.router({ 51 | getUser: t.procedure 52 | .input(z.string()) 53 | .query(({ input }) => { 54 | return { id: input, name: 'John Doe' }; 55 | }), 56 | 57 | createUser: t.procedure 58 | .input(z.object({ name: z.string() })) 59 | .mutation(({ input }) => { 60 | return { id: '1', ...input }; 61 | }), 62 | }); 63 | 64 | export type AppRouter = typeof appRouter; 65 | ``` 66 | 67 | #### Type-Safe Client 68 | 69 | With the router defined, tRPC ensures type safety on the client side: 70 | 71 | ```typescript 72 | import { createTRPCProxyClient, httpLink } from '@trpc/client'; 73 | import type { AppRouter } from './path/to/server'; 74 | 75 | const trpc = createTRPCProxyClient({ 76 | links: [ 77 | httpLink({ 78 | url: 'http://localhost:3000/trpc', 79 | }), 80 | ], 81 | }); 82 | 83 | async function main() { 84 | const user = await trpc.getUser.query('1'); 85 | console.log(user); // { id: '1', name: 'John Doe' } 86 | 87 | const newUser = await trpc.createUser.mutation({ name: 'Jane Doe' }); 88 | console.log(newUser); // { id: '1', name: 'Jane Doe' } 89 | } 90 | 91 | main(); 92 | ``` -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/generator.module.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger, Inject, Module, OnModuleInit } from '@nestjs/common'; 2 | import { DynamicModule } from '@nestjs/common/interfaces'; 3 | import { MetadataScanner } from '@nestjs/core'; 4 | import { CompilerOptions, ModuleKind, Project, ScriptTarget } from 'ts-morph'; 5 | 6 | import { TRPCGenerator } from './trpc.generator'; 7 | import { RouterGenerator } from './router.generator'; 8 | import { StaticGenerator } from './static.generator'; 9 | import { ContextGenerator } from './context.generator'; 10 | import { MiddlewareGenerator } from './middleware.generator'; 11 | import { DecoratorGenerator } from './decorator.generator'; 12 | import { ProcedureGenerator } from './procedure.generator'; 13 | import { 14 | TYPESCRIPT_APP_ROUTER_SOURCE_FILE, 15 | TYPESCRIPT_PROJECT, 16 | } from './generator.constants'; 17 | import { 18 | TRPC_GENERATOR_OPTIONS, 19 | TRPC_MODULE_CALLER_FILE_PATH, 20 | } from '../trpc.constants'; 21 | import { FactoryModule } from '../factories/factory.module'; 22 | import { ScannerModule } from '../scanners/scanner.module'; 23 | import * as path from 'node:path'; 24 | import { GeneratorModuleOptions } from './generator.interface'; 25 | 26 | @Module({ 27 | imports: [FactoryModule, ScannerModule], 28 | providers: [ 29 | // NestJS Providers 30 | ConsoleLogger, 31 | MetadataScanner, 32 | 33 | // Local Providers 34 | TRPCGenerator, 35 | RouterGenerator, 36 | ProcedureGenerator, 37 | DecoratorGenerator, 38 | MiddlewareGenerator, 39 | ContextGenerator, 40 | StaticGenerator, 41 | ], 42 | exports: [TRPCGenerator], 43 | }) 44 | export class GeneratorModule implements OnModuleInit { 45 | @Inject(TRPCGenerator) 46 | private readonly trpcGenerator!: TRPCGenerator; 47 | 48 | @Inject(TRPC_GENERATOR_OPTIONS) 49 | private readonly options!: GeneratorModuleOptions; 50 | 51 | static forRoot(options: GeneratorModuleOptions): DynamicModule { 52 | const defaultCompilerOptions: CompilerOptions = { 53 | target: ScriptTarget.ES2019, 54 | module: ModuleKind.CommonJS, 55 | emitDecoratorMetadata: true, 56 | experimentalDecorators: true, 57 | allowJs: true, 58 | checkJs: true, 59 | esModuleInterop: true, 60 | }; 61 | const project = new Project({ compilerOptions: defaultCompilerOptions }); 62 | 63 | const appRouterSourceFile = project.createSourceFile( 64 | path.resolve(options.outputDirPath ?? './', 'server.ts'), 65 | () => {}, 66 | { overwrite: true }, 67 | ); 68 | 69 | return { 70 | module: GeneratorModule, 71 | providers: [ 72 | { 73 | provide: TRPC_MODULE_CALLER_FILE_PATH, 74 | useValue: options.rootModuleFilePath, 75 | }, 76 | { provide: TYPESCRIPT_PROJECT, useValue: project }, 77 | { 78 | provide: TYPESCRIPT_APP_ROUTER_SOURCE_FILE, 79 | useValue: appRouterSourceFile, 80 | }, 81 | { provide: TRPC_GENERATOR_OPTIONS, useValue: options }, 82 | ], 83 | }; 84 | } 85 | 86 | async onModuleInit() { 87 | await this.trpcGenerator.generateSchemaFile(this.options.schemaFileImports); 88 | await this.trpcGenerator.generateHelpersFile(this.options.context); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/scanners/file.scanner.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as fs from 'node:fs'; 3 | import * as path from 'node:path'; 4 | import { SourceMapping } from '../interfaces/scanner.interface'; 5 | 6 | /** 7 | * For this specific file, using a static reference is desirable since `getCallerFilePath` uses a stack-trace to figure out the caller. 8 | * If this class is injected through dependency injection, that stack-trace will vary! 9 | */ 10 | @Injectable() 11 | export class FileScanner { 12 | public getCallerFilePath(skip: number = 2): string { 13 | const originalPrepareStackTrace = Error.prepareStackTrace; 14 | 15 | Error.prepareStackTrace = (_, stack) => stack; 16 | const error = new Error(); 17 | const stack = error.stack as unknown as NodeJS.CallSite[]; 18 | 19 | Error.prepareStackTrace = originalPrepareStackTrace; 20 | 21 | const caller = stack[skip]; 22 | const jsFilePath = caller?.getFileName(); 23 | 24 | if (jsFilePath == null) { 25 | throw new Error(`Could not find caller file: ${caller}`); 26 | } 27 | 28 | try { 29 | // Attempt to find the source map file and extract the original TypeScript path 30 | const sourceMap = this.getSourceMapFromJSPath(jsFilePath); 31 | return this.normalizePath( 32 | path.resolve(jsFilePath, '..', sourceMap.sources[0]), 33 | ); 34 | } catch (error) { 35 | // Suppress the warning if in test environment 36 | if (process.env.NODE_ENV !== 'test') { 37 | console.warn( 38 | `Warning: Could not resolve source map for ${jsFilePath}. Falling back to default path resolution.`, 39 | ); 40 | } 41 | return this.normalizePath(jsFilePath); 42 | } 43 | } 44 | 45 | private normalizePath(p: string): string { 46 | return path.resolve(p.replace(/\\/g, '/')); 47 | } 48 | 49 | private getPlatformPath(p: string): string { 50 | const exec = /^\/(\w*):(.*)/.exec(p); 51 | return /^win/.test(process.platform) && exec 52 | ? `${exec[1]}:\\${exec[2].replace(/\//g, '\\')}` 53 | : p; 54 | } 55 | 56 | private getSourceMapFromJSPath(sourcePath: string): SourceMapping { 57 | const SOURCE_MAP_REGEX = /\/\/# sourceMappingURL=(.*\.map)$/m; 58 | const filePath = this.getPlatformPath(sourcePath); 59 | 60 | let content: string; 61 | try { 62 | content = fs.readFileSync(filePath, { encoding: 'utf8' }); 63 | } catch (error) { 64 | throw new Error(`Could not read source file at path: ${filePath}`); 65 | } 66 | 67 | const exec = SOURCE_MAP_REGEX.exec(content); 68 | if (exec == null) { 69 | throw new Error( 70 | `Could not find source map comment in file at path ${sourcePath}. Make sure "sourceMap" is enabled in your tsconfig.`, 71 | ); 72 | } 73 | 74 | const sourceMapPath = path.resolve(filePath, '..', exec[1]); 75 | let sourceMapContent: string; 76 | try { 77 | sourceMapContent = fs.readFileSync(sourceMapPath, { encoding: 'utf8' }); 78 | } catch (error) { 79 | throw new Error( 80 | `Could not read source map file at path: ${sourceMapPath}`, 81 | ); 82 | } 83 | 84 | try { 85 | return JSON.parse(sourceMapContent); 86 | } catch (error) { 87 | throw new Error( 88 | `Failed to parse source map content from: ${sourceMapPath}`, 89 | ); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/factories/router.factory.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger, Inject, Injectable } from '@nestjs/common'; 2 | import { ModulesContainer } from '@nestjs/core'; 3 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 4 | import { camelCase } from 'lodash'; 5 | import { MIDDLEWARES_KEY, ROUTER_METADATA_KEY } from '../trpc.constants'; 6 | import { 7 | RouterInstance, 8 | TRPCPublicProcedure, 9 | TRPCRouter, 10 | } from '../interfaces/factory.interface'; 11 | import { TRPCMiddleware } from '../interfaces'; 12 | import { ProcedureFactory } from './procedure.factory'; 13 | import { Class, Constructor } from 'type-fest'; 14 | 15 | @Injectable() 16 | export class RouterFactory { 17 | @Inject(ConsoleLogger) 18 | private readonly consoleLogger!: ConsoleLogger; 19 | 20 | @Inject(ModulesContainer) 21 | private readonly modulesContainer!: ModulesContainer; 22 | 23 | @Inject(ProcedureFactory) 24 | private readonly procedureFactory!: ProcedureFactory; 25 | 26 | getRouters(): Array { 27 | const routers: Array = []; 28 | 29 | this.modulesContainer.forEach((moduleRef) => { 30 | moduleRef.providers.forEach((wrapper: InstanceWrapper) => { 31 | const router = this.extractRouterFromWrapper(wrapper); 32 | if (router != null) { 33 | routers.push(router); 34 | } 35 | }); 36 | }); 37 | 38 | return routers; 39 | } 40 | 41 | private extractRouterFromWrapper( 42 | wrapper: InstanceWrapper, 43 | ): RouterInstance | null { 44 | const { instance, name } = wrapper; 45 | 46 | if (instance == null) { 47 | return null; 48 | } 49 | 50 | const router = Reflect.getMetadata( 51 | ROUTER_METADATA_KEY, 52 | instance.constructor, 53 | ); 54 | 55 | if (router == null) { 56 | return null; 57 | } 58 | 59 | const middlewares: Array< 60 | Class | Constructor 61 | > = Reflect.getMetadata(MIDDLEWARES_KEY, instance.constructor) || []; 62 | 63 | return { 64 | name, 65 | instance, 66 | path: router.path, 67 | alias: router.alias, 68 | middlewares: middlewares, 69 | }; 70 | } 71 | 72 | serializeRoutes( 73 | router: TRPCRouter, 74 | procedure: TRPCPublicProcedure, 75 | ): Record { 76 | const routers = this.getRouters(); 77 | const routerSchema = Object.create({}); 78 | 79 | routers.forEach((route) => { 80 | const { instance, name, middlewares, alias } = route; 81 | const camelCasedRouterName = camelCase(alias ?? name); 82 | const prototype = Object.getPrototypeOf(instance); 83 | 84 | const procedures = this.procedureFactory.getProcedures( 85 | instance, 86 | prototype, 87 | ); 88 | 89 | this.consoleLogger.log( 90 | `Router ${name} as ${camelCasedRouterName}.`, 91 | 'Router Factory', 92 | ); 93 | 94 | const routerProcedures = this.procedureFactory.serializeProcedures( 95 | procedures, 96 | instance, 97 | camelCasedRouterName, 98 | procedure, 99 | middlewares, 100 | ); 101 | 102 | // TODO: To get this working with `trpc` v11, we need to remove the `router()` method from here. 103 | routerSchema[camelCasedRouterName] = router(routerProcedures); 104 | }); 105 | 106 | return routerSchema; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/trpc.driver.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger, Inject, Injectable, Type } from '@nestjs/common'; 2 | import { HttpAdapterHost, ModuleRef } from '@nestjs/core'; 3 | import type { Application as ExpressApplication } from 'express'; 4 | import type { FastifyInstance as FastifyApplication } from 'fastify'; 5 | import { TRPCContext, TRPCModuleOptions } from './interfaces'; 6 | import { AnyRouter, initTRPC } from '@trpc/server'; 7 | import { TRPCFactory } from './factories/trpc.factory'; 8 | import { AppRouterHost } from './app-router.host'; 9 | import { ExpressDriver, FastifyDriver } from './drivers'; 10 | 11 | function isExpressApplication(app: any): app is ExpressApplication { 12 | return ( 13 | typeof app === 'function' && 14 | typeof app.get === 'function' && 15 | typeof app.post === 'function' && 16 | typeof app.use === 'function' && 17 | typeof app.listen === 'function' 18 | ); 19 | } 20 | 21 | function isFastifyApplication(app: any): app is FastifyApplication { 22 | return ( 23 | typeof app === 'object' && 24 | app !== null && 25 | typeof app.get === 'function' && 26 | typeof app.post === 'function' && 27 | typeof app.register === 'function' && 28 | typeof app.listen === 'function' 29 | ); 30 | } 31 | 32 | @Injectable() 33 | export class TRPCDriver< 34 | TOptions extends Record = TRPCModuleOptions, 35 | > { 36 | @Inject(HttpAdapterHost) 37 | protected readonly httpAdapterHost!: HttpAdapterHost; 38 | 39 | @Inject(TRPCFactory) 40 | protected readonly trpcFactory!: TRPCFactory; 41 | 42 | @Inject(ConsoleLogger) 43 | protected readonly consoleLogger!: ConsoleLogger; 44 | 45 | @Inject(AppRouterHost) 46 | protected readonly appRouterHost!: AppRouterHost; 47 | 48 | @Inject(ExpressDriver) 49 | protected readonly expressDriver!: ExpressDriver; 50 | 51 | @Inject(FastifyDriver) 52 | protected readonly fastifyDriver!: FastifyDriver; 53 | 54 | constructor(private moduleRef: ModuleRef) {} 55 | 56 | public async start(options: TRPCModuleOptions) { 57 | //@ts-expect-error Ignoring typescript here since it's the same type, yet it still isn't able to infer it. 58 | const { procedure, router } = initTRPC.context().create({ 59 | ...(options.transformer != null 60 | ? { transformer: options.transformer } 61 | : {}), 62 | ...(options.errorFormatter != null 63 | ? { errorFormatter: options.errorFormatter } 64 | : {}), 65 | }); 66 | 67 | const appRouter: AnyRouter = this.trpcFactory.serializeAppRoutes( 68 | router, 69 | procedure, 70 | ); 71 | 72 | this.appRouterHost.appRouter = appRouter; 73 | 74 | const contextClass = options.context; 75 | const contextInstance = 76 | contextClass != null 77 | ? this.moduleRef.get, TRPCContext>(contextClass, { 78 | strict: false, 79 | }) 80 | : null; 81 | 82 | const { httpAdapter } = this.httpAdapterHost; 83 | const platformName = httpAdapter.getType(); 84 | 85 | const app = httpAdapter.getInstance< 86 | ExpressApplication | FastifyApplication 87 | >(); 88 | 89 | if (platformName === 'express' && isExpressApplication(app)) { 90 | await this.expressDriver.start(options, app, appRouter, contextInstance); 91 | } else if (platformName === 'fastify' && isFastifyApplication(app)) { 92 | await this.fastifyDriver.start(options, app, appRouter, contextInstance); 93 | } else { 94 | throw new Error(`Unsupported http adapter: ${platformName}`); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /docs/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { MegaphoneIcon, ArrowRight, DollarSignIcon, CopyIcon, CheckIcon, CodeIcon, ZapIcon, ShieldCheckIcon } from 'lucide-react'; 2 | import { Preview } from '../Preview'; 3 | import Link from "next/link"; 4 | import { useCopyToClipboard } from "@uidotdev/usehooks"; 5 | import { useState } from 'react'; 6 | import FeatureCard from '../FeatureCard'; 7 | 8 | export default function Home() { 9 | const [copiedText, copyToClipboard] = useCopyToClipboard(); 10 | const [hasCopied, changeHasCopied] = useState(false); 11 | 12 | const handleCopy = () => { 13 | copyToClipboard("npm install nestjs-trpc"); 14 | changeHasCopied(true); 15 | setTimeout(()=> { 16 | changeHasCopied(false); 17 | }, 3000) 18 | } 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | Read about NestJS tRPC 1.0 Launch 27 | 28 | 29 | 30 | 31 | Bring type-safetyto NestJS 32 | 33 | Discover how to write type-safe end-to-end apis using tRPC in NestJS. 34 | 35 | {handleCopy()}} className={"flex gap-3 rounded-full border border-[#3D596E] h-full py-4 px-6 items-center transition-all hover:bg-[#3D596E]"}> 36 | 37 | npm install nestjs-trpc 38 | { 39 | hasCopied ? : 40 | } 41 | 42 | 43 | 44 | Documentation 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Why use NestJS tRPC? 56 | It's the best way to write NestJS APIs since the GraphQL adapter! 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ) 66 | } -------------------------------------------------------------------------------- /docs/theme.config.jsx: -------------------------------------------------------------------------------- 1 | import { Footer } from './components/Footer'; 2 | 3 | export default { 4 | logo: NestJS tRPC, 5 | primaryHue: 200, 6 | primarySaturation: 100, 7 | project: { 8 | link: 'https://github.com/KevinEdry/nestjs-trpc', 9 | }, 10 | footer: { 11 | component: 12 | }, 13 | banner: { 14 | key: '1.0-release', 15 | text: ( 16 | 17 | 🎉 NestJS tRPC 1.0 is released. Read more → 18 | 19 | ), 20 | dismissible: true 21 | }, 22 | chat: { 23 | link: 'https://discord.gg/trpc-867764511159091230', 24 | }, 25 | docsRepositoryBase: 'https://github.com/KevinEdry/nestjs-trpc', 26 | sidebar: { 27 | defaultMenuCollapseLevel: 1, 28 | }, 29 | nextThemes: { 30 | defaultTheme: 'dark', 31 | forcedTheme: 'dark', 32 | }, 33 | themeSwitch: { 34 | component: null, 35 | }, 36 | useNextSeoProps() { 37 | return { 38 | defaultTitle: "NestJS-tRPC: Bringing type-safety to NestJS", 39 | additionalLinkTags: [ 40 | { 41 | rel: "apple-touch-icon", 42 | sizes: "180x180", 43 | href: "/favicon/apple-touch-icon.png" 44 | }, 45 | { 46 | rel: "manifest", 47 | href: "/favicon/site.webmanifest" 48 | }, 49 | ...[16, 32].map(size => ({ 50 | rel: "icon", 51 | type: "image/png", 52 | sizes: `${size}x${size}`, 53 | href: `/favicon/favicon-${size}x${size}.png` 54 | })), 55 | { 56 | rel: "canonical", 57 | href: "https://nestjs-trpc.io/" 58 | } 59 | ], 60 | additionalMetaTags: [ 61 | { 62 | name: "viewport", 63 | content: "width=device-width, initial-scale=1.0" 64 | }, 65 | { 66 | charset: "utf-8", 67 | }, 68 | { 69 | property: "og:image", 70 | content: "/og.jpg" 71 | }, 72 | { 73 | property: "og:type", 74 | content: "object" 75 | }, 76 | { 77 | property: "og:title", 78 | content: "NestJS-tRPC: Bringing type-safety to NestJS", 79 | }, 80 | { 81 | property: "og:description", 82 | content: "NestJS tRPC is a library designed to integrate the capabilities of tRPC into the NestJS framework. It aims to provide native support for decorators and implement an opinionated approach that aligns with NestJS conventions." 83 | }, 84 | { 85 | property: "description", 86 | content: "NestJS tRPC is a library designed to integrate the capabilities of tRPC into the NestJS framework. It aims to provide native support for decorators and implement an opinionated approach that aligns with NestJS conventions." 87 | }, 88 | { 89 | property: "og:site_name", 90 | content: "NestJS-tRPC: Bringing type-safety to NestJS" 91 | }, 92 | { 93 | property: "og:url", 94 | content: "https://nestjs-trpc.io/" 95 | }, 96 | { 97 | name: "twitter:card", 98 | content: "summary" 99 | }, 100 | { 101 | name: "twitter:image", 102 | content: "https://nestjs-trpc.io/banner.png" 103 | }, 104 | { 105 | name: "twitter:title", 106 | content: "NestJS-tRPC: Bringing type-safety to NestJS" 107 | }, 108 | { 109 | name: "twitter:description", 110 | content: "NestJS-tRPC: Bringing type-safety to NestJS" 111 | }, 112 | { 113 | name: "twitter:site", 114 | content: "@KevinEdry" 115 | } 116 | ] 117 | } 118 | }, 119 | } -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/__tests__/decorator.generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { DecoratorGenerator } from '../decorator.generator'; 3 | import { ConsoleLogger } from '@nestjs/common'; 4 | import { Project, SourceFile } from 'ts-morph'; 5 | import { ProcedureGenerator } from '../procedure.generator'; 6 | 7 | describe('DecoratorGenerator', () => { 8 | let decoratorGenerator: DecoratorGenerator; 9 | let consoleLogger: jest.Mocked; 10 | let project: Project; 11 | let sourceFile: SourceFile; 12 | 13 | beforeEach(async () => { 14 | project = new Project(); 15 | sourceFile = project.createSourceFile("test.ts", ` 16 | import { Query, Mutation, UseMiddlewares } from '@nestjs/common'; 17 | 18 | class TestClass { 19 | @Query() 20 | queryMethod() {} 21 | 22 | @Mutation() 23 | mutationMethod() {} 24 | 25 | @UseMiddlewares() 26 | middlewareMethod() {} 27 | 28 | @UnsupportedDecorator() 29 | unsupportedMethod() {} 30 | } 31 | `, {overwrite: true}); 32 | 33 | const module: TestingModule = await Test.createTestingModule({ 34 | providers: [ 35 | DecoratorGenerator, 36 | { 37 | provide: ConsoleLogger, 38 | useValue: { 39 | warn: jest.fn(), 40 | }, 41 | }, 42 | { 43 | provide: ProcedureGenerator, 44 | useValue: { 45 | warn: jest.fn(), 46 | }, 47 | }, 48 | ], 49 | }).compile(); 50 | 51 | decoratorGenerator = module.get(DecoratorGenerator); 52 | consoleLogger = module.get(ConsoleLogger); 53 | }); 54 | 55 | it('should be defined', () => { 56 | expect(decoratorGenerator).toBeDefined(); 57 | }); 58 | 59 | describe('serializeProcedureDecorators', () => { 60 | it('should serialize Query decorator', () => { 61 | const queryMethod = sourceFile.getClass('TestClass')!.getMethod('queryMethod')!; 62 | const queryDecorator = queryMethod.getDecorator('Query')!; 63 | 64 | const result = decoratorGenerator.serializeProcedureDecorators( 65 | [queryDecorator], 66 | sourceFile, 67 | project 68 | ); 69 | 70 | expect(result).toEqual([{ name: 'Query', arguments: {} }]); 71 | }); 72 | 73 | it('should serialize Mutation decorator', () => { 74 | const mutationMethod = sourceFile.getClass('TestClass')!.getMethod('mutationMethod')!; 75 | const mutationDecorator = mutationMethod.getDecorator('Mutation')!; 76 | 77 | const result = decoratorGenerator.serializeProcedureDecorators( 78 | [mutationDecorator], 79 | sourceFile, 80 | project 81 | ); 82 | 83 | expect(result).toEqual([{ name: 'Mutation', arguments: {} }]); 84 | }); 85 | 86 | it('should ignore UseMiddlewares decorator', () => { 87 | const middlewareMethod = sourceFile.getClass('TestClass')!.getMethod('middlewareMethod')!; 88 | const middlewaresDecorator = middlewareMethod.getDecorator('UseMiddlewares')!; 89 | 90 | const result = decoratorGenerator.serializeProcedureDecorators( 91 | [middlewaresDecorator], 92 | sourceFile, 93 | project 94 | ); 95 | 96 | expect(result).toEqual([]); 97 | }); 98 | 99 | it('should warn about unsupported decorators', () => { 100 | const unsupportedMethod = sourceFile.getClass('TestClass')!.getMethod('unsupportedMethod')!; 101 | const unsupportedDecorator = unsupportedMethod.getDecorator('UnsupportedDecorator')!; 102 | 103 | decoratorGenerator.serializeProcedureDecorators( 104 | [unsupportedDecorator], 105 | sourceFile, 106 | project 107 | ); 108 | 109 | expect(consoleLogger.warn).toHaveBeenCalledWith('Decorator UnsupportedDecorator, not supported.'); 110 | }); 111 | }); 112 | }); -------------------------------------------------------------------------------- /docs/pages/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC Documentation - Installation Guide" 3 | --- 4 | 5 | 6 | import { Callout, Tabs, Steps } from 'nextra/components'; 7 | import Link from 'next/link'; 8 | import Table from '../../components/Table'; 9 | 10 | # Installation 11 | 12 | This document will guide you through the installation and setting up of **NestJS tRPC** in your nestjs project. 13 | 14 | 15 | If you don't have a NestJS project setup yet, please visit our 16 | NestJS Quickstart Guide 17 | or check out the official 18 | NestJS documentation. 19 | 20 | 21 | 22 | ### Manual Installation 23 | To install **NestJS tRPC** with your preferred package manager, you can use any of the following commands: 24 | 25 | 26 | 27 | ```bash copy 28 | npm install nestjs-trpc 29 | ``` 30 | 31 | 32 | ```bash copy 33 | pnpm add nestjs-trpc 34 | ``` 35 | 36 | 37 | ```bash copy 38 | yarn add nestjs-trpc 39 | ``` 40 | 41 | 42 | ```bash copy 43 | bun install nestjs-trpc 44 | ``` 45 | 46 | 47 | 48 | ### Initialization 49 | 50 | Once the packages are installed, we can import the `TRPCModule` and configure it with the `forRoot()` static method. 51 | 52 | ```typescript filename="app.module.ts" 53 | import { Module } from '@nestjs/common'; 54 | import { TRPCModule } from 'nestjs-trpc'; 55 | 56 | @Module({ 57 | imports: [ 58 | TRPCModule.forRoot({ 59 | autoSchemaFile: './src/@generated', 60 | }), 61 | ], 62 | }) 63 | export class AppModule {} 64 | ``` 65 | 66 | The `forRoot()` method takes an options object as an argument. These options are passed through to the underlying `express` or `fastify` driver. For example, if you want to disable the schema file generation (when deploying to production), you can omit the `autoSchemaFile` option. 67 | 68 | ### Start your NestJS server. You should be good to go! 🎉 69 | 70 | 71 | 72 | 73 | When setting up **NestJS tRPC** you must set your `"sourceMap": true{:typescript}` in your `compilerOptions` within the `tsconfig.json` configuration file. 74 | 75 | 76 | ### Module Options 77 | 78 | You can import `TRPCModuleOptions` type from `nestjs-trpc` to safely assert all of the `TRPCModule` option types. 79 | 80 | ```typescript 81 | import { TRPCModule, TRPCModuleOptions } from 'nestjs-trpc'; 82 | const trpcOptions: TRPCModuleOptions = { 83 | autoSchemaFile: './src/@generated', 84 | }; 85 | ``` 86 | 87 | 88 | 89 | `basePath` 90 | `string` 91 | `"/trpc"` 92 | 93 | 94 | `autoSchemaFile` 95 | `string` 96 | - 97 | 98 | 99 | `schemaFileImports` 100 | `Array` 101 | - 102 | 103 | 104 | `context` 105 | `TRPCContext` 106 | - 107 | 108 | 109 | `transformer` 110 | `unknown` 111 | - 112 | 113 | 114 | `errorFormatter` 115 | `(opts: { shape, error }) => {}` 116 | - 117 | 118 | 119 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/generators/middleware.generator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassDeclaration, 3 | Node, 4 | Project, 5 | MethodDeclaration, 6 | Type, 7 | SyntaxKind, 8 | SourceFile, 9 | OptionalKind, 10 | PropertySignatureStructure, 11 | } from 'ts-morph'; 12 | import { Injectable } from '@nestjs/common'; 13 | import { TRPCMiddleware } from '../interfaces'; 14 | import type { Class } from 'type-fest'; 15 | 16 | @Injectable() 17 | export class MiddlewareGenerator { 18 | public async getMiddlewareInterface( 19 | routerFilePath: string, 20 | middleware: Class, 21 | project: Project, 22 | ): Promise<{ 23 | name: string; 24 | properties: Array>; 25 | } | null> { 26 | const className = middleware.name; 27 | if (!className) { 28 | return null; 29 | } 30 | 31 | const middlewareInstance = new middleware(); 32 | 33 | if (typeof middlewareInstance.use !== 'function') { 34 | return null; 35 | } 36 | 37 | const contextSourceFile = project.addSourceFileAtPath(routerFilePath); 38 | 39 | const classDeclaration = this.getClassDeclaration( 40 | contextSourceFile, 41 | middleware.name, 42 | ); 43 | 44 | if (!classDeclaration) { 45 | return null; 46 | } 47 | 48 | const useMethod = classDeclaration.getMethod('use'); 49 | if (!useMethod) { 50 | return null; 51 | } 52 | 53 | const ctxType = this.extractCtxTypeFromUseMethod(useMethod); 54 | if (!ctxType) { 55 | return null; 56 | } 57 | 58 | return { 59 | name: className, 60 | properties: this.typeToProperties(ctxType), 61 | }; 62 | } 63 | 64 | private extractCtxTypeFromUseMethod( 65 | useMethod: MethodDeclaration, 66 | ): Type | null { 67 | const body = useMethod.getBody(); 68 | if (!body) return null; 69 | 70 | // Find the call to opts.next() 71 | const nextCall = body 72 | .getDescendantsOfKind(SyntaxKind.CallExpression) 73 | .find((call) => { 74 | const expression = call.getExpression(); 75 | return ( 76 | Node.isPropertyAccessExpression(expression) && 77 | expression.getName() === 'next' && 78 | Node.isIdentifier(expression.getExpression()) && 79 | expression.getExpression().getText() === 'opts' 80 | ); 81 | }); 82 | 83 | if (!nextCall) return null; 84 | 85 | // Get the argument passed to opts.next() 86 | const nextArg = nextCall.getArguments()[0]; 87 | if (!Node.isObjectLiteralExpression(nextArg)) return null; 88 | 89 | // Find the 'ctx' property in the argument 90 | const ctxProperty = nextArg 91 | .getProperties() 92 | .find( 93 | (prop) => Node.isPropertyAssignment(prop) && prop.getName() === 'ctx', 94 | ); 95 | 96 | if (!Node.isPropertyAssignment(ctxProperty)) return null; 97 | 98 | // Get the type of the 'ctx' property value 99 | return ctxProperty.getInitializer()?.getType() || null; 100 | } 101 | 102 | private getClassDeclaration( 103 | sourceFile: SourceFile, 104 | className: string, 105 | ): ClassDeclaration | undefined { 106 | const classDeclaration = sourceFile.getClass(className); 107 | if (classDeclaration) { 108 | return classDeclaration; 109 | } 110 | return undefined; 111 | } 112 | 113 | private typeToProperties( 114 | type: Type, 115 | ): Array> { 116 | const properties: Array> = []; 117 | 118 | if (type.isObject()) { 119 | type.getProperties().forEach((prop) => { 120 | const propValueDeclaration = prop.getValueDeclaration(); 121 | if (propValueDeclaration != null) { 122 | properties.push({ 123 | name: prop.getName(), 124 | type: prop.getTypeAtLocation(propValueDeclaration).getText(), 125 | }); 126 | } 127 | }); 128 | } 129 | 130 | return properties; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /docs/pages/docs/integrations.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NestJS-tRPC Documentation - Integrations" 3 | --- 4 | 5 | import { Callout, Steps, Tabs } from 'nextra-theme-docs'; 6 | 7 | # Integrations 8 | 9 | ### Accessing the AppRouter 10 | In some circumstances ,for example end-to-end tests or integrating with other `trpc` plugins, you may want to get a reference to the generated appRouter object. In end-to-end tests, you can then run queries using the appRouter object directly without using any HTTP listeners. 11 | 12 | You can access the generated appRouter using the `AppRouterHost` class: 13 | 14 | ```typescript 15 | const { appRouter } = app.get(AppRouterHost); 16 | ``` 17 | 18 | You must call the `AppRouterHost.appRouter{:tsx}` getter after the application has been initialized (after the `onModuleInit` hook has been triggered by either the `app.listen(){:tsx}` or `app.init(){:tsx}` method). 19 | 20 | 21 | With the runtime appRouter object, you can integrate with virtually any trpc-specific library, this includes [trpc-panel](https://github.com/iway1/trpc-panel), [trpc-openapi](https://github.com/jlalmes/trpc-openapi), [trpc-playground](https://github.com/sachinraja/trpc-playground) and more. 22 | 23 | ### Integrating with `trpc-panel` 24 | Here is a short example of how to integrate [trpc-panel](https://github.com/iway1/trpc-panel), while this is a specific integration, the techniques used here can be applied to any of the trpc-specific libraries you wish to include. 25 | If you still run into issues, please open a [new issue](https://github.com/KevinEdry/nestjs-trpc/issues/new) or contact us via Discord. 26 | 27 | 28 | ### Installation 29 | To install `trpc-panel` with your preferred package manager, you can use any of the following commands: 30 | 31 | 32 | ```bash copy 33 | npm install trpc-panel 34 | ``` 35 | 36 | 37 | ```bash copy 38 | pnpm add trpc-panel 39 | ``` 40 | 41 | 42 | ```bash copy 43 | yarn add trpc-panel 44 | ``` 45 | 46 | 47 | ```bash copy 48 | bun install trpc-panel 49 | ``` 50 | 51 | 52 | 53 | ### Creating the Panel Controller 54 | As per the `trpc-panel` [docs](https://github.com/iway1/trpc-panel?tab=readme-ov-file#quick-start) we need to serve the panel, using the `renderTrpcPanel` method for every method on route `/panel`. 55 | 56 | ```typescript {8, 14-16} filename="trpc-panel.controller.ts" copy 57 | import { All, Controller, Inject, OnModuleInit } from '@nestjs/common'; 58 | import { renderTrpcPanel } from 'trpc-panel'; 59 | import { AnyRouter } from '@trpc/server'; 60 | import { AppRouterHost } from 'nestjs-trpc'; 61 | 62 | @Controller() 63 | export class TrpcPanelController implements OnModuleInit { 64 | private appRouter!: AnyRouter; 65 | 66 | constructor( 67 | @Inject(AppRouterHost) private readonly appRouterHost: AppRouterHost, 68 | ) {} 69 | 70 | onModuleInit() { 71 | this.appRouter = this.appRouterHost.appRouter; 72 | } 73 | 74 | @All('/panel') 75 | panel(): string { 76 | return renderTrpcPanel(this.appRouter, { 77 | url: 'http://localhost:8080/trpc', 78 | }); 79 | } 80 | } 81 | ``` 82 | 83 | As you can see in the example above, we are using `onModuleInit` lifecycle method to make sure the appRouter is initialized and available. 84 | 85 | 86 | ### Register the Controller 87 | In order to apply the new Controller routes, we need to register it in the `app.module.ts` file. 88 | 89 | ```typescript /TrpcPanelController/ filename="app.module.ts" copy 90 | @Module({ 91 | imports: [ 92 | TRPCModule.forRoot({ 93 | autoSchemaFile: './src/@generated', 94 | context: AppContext, 95 | }), 96 | ], 97 | controllers: [TrpcPanelController], 98 | providers: [], 99 | }) 100 | export class AppModule {} 101 | ``` 102 | 103 | 104 | Once you've registered the controller, start your NestJS application. You can then access the tRPC panel at https://localhost:8080/panel. 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Nestjs tRPC Adapter 10 | An opinionated approach to buildingEnd-to-end typesafe APIs with tRPC within NestJS. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | The client above is not importing any code from the server, only its type declarations. 26 | 27 | 28 | 29 | 30 | 31 | ## Introduction 32 | 33 | **NestJS tRPC** is a library designed to integrate the capabilities of tRPC into the NestJS framework. It aims to provide native support for decorators and implement an opinionated approach that aligns with NestJS conventions. 34 | 35 | ## Features 36 | 37 | - ✅ Supports most tRPC features out of the box with more to come. 38 | - 🧙 Full static typesafety & autocompletion on the client, for inputs, outputs, and errors. 39 | - 🙀 Implements the Nestjs opinionated approach to how tRPC works. 40 | - ⚡️ Same client-side DX - We generate the AppRouter on the fly. 41 | - 🔋 Examples are available in the ./examples folder. 42 | - 📦 Out of the box support for **Dependency Injection** within the routes and procedures. 43 | - 👀 Native support for `express`, `fastify`, and `zod` with more drivers to come! 44 | 45 | ## Quickstart 46 | 47 | ### Installation 48 | 49 | To install **NestJS tRPC** with your preferred package manager, you can use any of the following commands: 50 | 51 | ```shell 52 | # npm 53 | npm install nestjs-trpc zod @trpc/server 54 | 55 | # pnpm 56 | pnpm add nestjs-trpc zod @trpc/server 57 | 58 | # yarn 59 | yarn add nestjs-trpc zod @trpc/server 60 | ``` 61 | 62 | ## How to use 63 | 64 | Here's a brief example demonstrating how to use the decorators available in **NestJS tRPC**: 65 | 66 | ```typescript 67 | // users.router.ts 68 | import { Inject } from '@nestjs/common'; 69 | import { Router, Query, UseMiddlewares } from 'nestjs-trpc'; 70 | import { UserService } from './user.service'; 71 | import { ProtectedMiddleware } from './protected.middleware'; 72 | import { TRPCError } from '@trpc/server'; 73 | import { z } from 'zod'; 74 | 75 | const userSchema = z.object({ 76 | name: z.string(), 77 | password: z.string() 78 | }) 79 | 80 | @Router() 81 | class UserRouter { 82 | constructor( 83 | @Inject(UserService) private readonly userService: UserService 84 | ) {} 85 | 86 | @UseMiddlewares(ProtectedMiddleware) 87 | @Query({ output: z.array(userSchema) }) 88 | async getUsers() { 89 | try { 90 | return this.userService.getUsers(); 91 | } catch (error: unknown) { 92 | throw new TRPCError({ 93 | code: "INTERNAL_SERVER_ERROR", 94 | message: "An error has occured when trying to get users.", 95 | cause: error 96 | }) 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | **👉 See full documentation on [NestJS-tRPC.io](https://nestjs-trpc.io/docs). 👈** 103 | 104 | ## All contributors 105 | 106 | > NestJS tRPC is developed by [Kevin Edry](https://twitter.com/KevinEdry), which taken a huge inspiration from both NestJS and tRPC inner workings. 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/decorators/middlewares.decorator.ts: -------------------------------------------------------------------------------- 1 | import type { Class, Constructor } from 'type-fest'; 2 | import type { TRPCMiddleware } from '../interfaces'; 3 | import { MIDDLEWARES_KEY } from '../trpc.constants'; 4 | import { isFunction } from 'lodash'; 5 | import { validateEach } from '../utils/validate-each.util'; 6 | 7 | /** 8 | * TODO: Generate Return Context Type. 9 | * 10 | * Decorator that binds middlewares to the scope of the router or a procedure, 11 | * depending on its context. 12 | * 13 | * When `@UseMiddlewares` is used at the router level, the middleware will be 14 | * applied to every handler (method) in the router. 15 | * 16 | * When `@UseMiddlewares` is used at the individual handler level, the middleware 17 | * will apply only to that specific method. 18 | * 19 | * @param middlewares a single middleware instance or class, or a list of comma separated middleware instances 20 | * or classes. 21 | * 22 | * @see [Middlewares](https://nestjs-trpc.io/docs/middlewares) 23 | * 24 | * @publicApi 25 | */ 26 | export function UseMiddlewares( 27 | ...middlewares: Array | Constructor> 28 | ): MethodDecorator & ClassDecorator { 29 | return ( 30 | target: any, 31 | key?: string | symbol, 32 | descriptor?: TypedPropertyDescriptor, 33 | ) => { 34 | const isMiddlewareValid = ( 35 | middleware: Constructor | Record, 36 | ) => 37 | middleware && 38 | (isFunction(middleware) || 39 | isFunction((middleware as Record).use)); 40 | 41 | if (descriptor) { 42 | validateEach( 43 | target.constructor, 44 | middlewares, 45 | isMiddlewareValid, 46 | '@UseMiddlewares', 47 | 'middleware', 48 | ); 49 | Reflect.defineMetadata( 50 | MIDDLEWARES_KEY, 51 | [...middlewares], 52 | descriptor.value, 53 | ); 54 | return descriptor; 55 | } 56 | validateEach( 57 | target.constructor, 58 | middlewares, 59 | isMiddlewareValid, 60 | '@UseMiddlewares', 61 | 'middleware', 62 | ); 63 | Reflect.defineMetadata(MIDDLEWARES_KEY, [...middlewares], target); 64 | return target; 65 | }; 66 | } 67 | 68 | /** 69 | * @deprecated Use `@UseMiddlewares` instead. This decorator is deprecated 70 | * in order to satisfy NestJS naming convention fe. `@UseGuards`. 71 | * 72 | * Decorator that binds middlewares to the scope of the router or a procedure, 73 | * depending on its context. 74 | * 75 | * When `@Middlewares` is used at the router level, the middleware will be 76 | * applied to every handler (method) in the router. 77 | * 78 | * When `@Middlewares` is used at the individual handler level, the middleware 79 | * will apply only to that specific method. 80 | * 81 | * @param middlewares a single middleware instance or class, or a list of comma separated middleware instances 82 | * or classes. 83 | * 84 | * @see [Middlewares](https://nestjs-trpc.io/docs/middlewares) 85 | * 86 | * @publicApi 87 | */ 88 | export function Middlewares( 89 | ...middlewares: Array | Constructor> 90 | ): MethodDecorator & ClassDecorator { 91 | return ( 92 | target: any, 93 | key?: string | symbol, 94 | descriptor?: TypedPropertyDescriptor, 95 | ) => { 96 | const isMiddlewareValid = ( 97 | middleware: Constructor | Record, 98 | ) => 99 | middleware && 100 | (isFunction(middleware) || 101 | isFunction((middleware as Record).use)); 102 | 103 | if (descriptor) { 104 | validateEach( 105 | target.constructor, 106 | middlewares, 107 | isMiddlewareValid, 108 | '@Middlewares', 109 | 'middleware', 110 | ); 111 | Reflect.defineMetadata( 112 | MIDDLEWARES_KEY, 113 | [...middlewares], 114 | descriptor.value, 115 | ); 116 | return descriptor; 117 | } 118 | validateEach( 119 | target.constructor, 120 | middlewares, 121 | isMiddlewareValid, 122 | '@Middlewares', 123 | 'middleware', 124 | ); 125 | Reflect.defineMetadata(MIDDLEWARES_KEY, [...middlewares], target); 126 | return target; 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Nestjs tRPC Adapter 10 | An opinionated approach to buildingEnd-to-end typesafe APIs with tRPC within NestJS. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | The client above is not importing any code from the server, only its type declarations. 26 | 27 | 28 | 29 | 30 | 31 | ## Introduction 32 | 33 | **NestJS tRPC** is a library designed to integrate the capabilities of tRPC into the NestJS framework. It aims to provide native support for decorators and implement an opinionated approach that aligns with NestJS conventions. 34 | 35 | ## Features 36 | 37 | - ✅ Supports most tRPC features out of the box with more to come. 38 | - 🧙 Full static typesafety & autocompletion on the client, for inputs, outputs, and errors. 39 | - 🙀 Implements the Nestjs opinionated approach to how tRPC works. 40 | - ⚡️ Same client-side DX - We generate the AppRouter on the fly. 41 | - 🔋 Examples are available in the ./examples folder. 42 | - 📦 Out of the box support for **Dependency Injection** within the routes and procedures. 43 | - 👀 Native support for `express`, `fastify`, and `zod` with more drivers to come! 44 | 45 | ## Quickstart 46 | 47 | ### Installation 48 | 49 | To install **NestJS tRPC** with your preferred package manager, you can use any of the following commands: 50 | 51 | ```shell 52 | # npm 53 | npm install trpc-nestjs zod @trpc/server 54 | 55 | # pnpm 56 | pnpm add trpc-nestjs zod @trpc/server 57 | 58 | # yarn 59 | yarn add trpc-nestjs zod @trpc/server 60 | ``` 61 | 62 | ## How to use 63 | 64 | Here's a brief example demonstrating how to use the decorators available in **NestJS tRPC**: 65 | 66 | ```typescript 67 | // users.router.ts 68 | import { Inject } from '@nestjs/common'; 69 | import { Router, Query, UseMiddlewares } from 'trpc-nestjs'; 70 | import { UserService } from './user.service'; 71 | import { ProtectedMiddleware } from './protected.middleware'; 72 | import { TRPCError } from '@trpc/server'; 73 | import { z } from 'zod'; 74 | 75 | const userSchema = z.object({ 76 | name: z.string(), 77 | password: z.string() 78 | }) 79 | 80 | @Router() 81 | class UserRouter { 82 | constructor( 83 | @Inject(UserService) private readonly userService: UserService 84 | ) {} 85 | 86 | @UseMiddlewares(ProtectedMiddleware) 87 | @Query({ output: z.array(userSchema) }) 88 | async getUsers() { 89 | try { 90 | return this.userService.getUsers(); 91 | } catch (error: unknown) { 92 | throw new TRPCError({ 93 | code: "INTERNAL_SERVER_ERROR", 94 | message: "An error has occured when trying to get users.", 95 | cause: error 96 | }) 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | **👉 See full documentation on [NestJS-tRPC.io](https://nestjs-trpc.io/docs). 👈** 103 | 104 | ## All contributors 105 | 106 | > NestJS tRPC is developed by [Kevin Edry](https://twitter.com/KevinEdry), which taken a huge inspiration from both NestJS and tRPC inner workings. 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /packages/nestjs-trpc/lib/scanners/imports.scanner.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Project, SourceFile } from 'ts-morph'; 3 | import { SourceFileImportsMap } from '../interfaces/generator.interface'; 4 | 5 | @Injectable() 6 | export class ImportsScanner { 7 | public buildSourceFileImportsMap( 8 | sourceFile: SourceFile, 9 | project: Project, 10 | ): Map { 11 | const sourceFileImportsMap = new Map(); 12 | const importDeclarations = sourceFile.getImportDeclarations(); 13 | 14 | for (const importDeclaration of importDeclarations) { 15 | const namedImports = importDeclaration.getNamedImports(); 16 | for (const namedImport of namedImports) { 17 | const name = namedImport.getName(); 18 | const importedSourceFile = 19 | importDeclaration.getModuleSpecifierSourceFile(); 20 | 21 | if (importedSourceFile == null) { 22 | continue; 23 | } 24 | 25 | const resolvedSourceFile = 26 | importedSourceFile.getFilePath().endsWith('index.ts') && 27 | !importedSourceFile.getVariableDeclaration(name) 28 | ? this.resolveBarrelFileImport(importedSourceFile, name, project) 29 | : importedSourceFile; 30 | 31 | if (resolvedSourceFile == null) { 32 | continue; 33 | } 34 | 35 | // Generalized logic to handle various kinds of declarations 36 | const declaration = 37 | resolvedSourceFile.getVariableDeclaration(name) || 38 | resolvedSourceFile.getClass(name) || 39 | resolvedSourceFile.getInterface(name) || 40 | resolvedSourceFile.getEnum(name) || 41 | resolvedSourceFile.getFunction(name); 42 | 43 | if (declaration != null) { 44 | const initializer = 45 | 'getInitializer' in declaration 46 | ? declaration.getInitializer() 47 | : declaration; 48 | sourceFileImportsMap.set(name, { 49 | initializer: initializer ?? declaration, 50 | sourceFile: resolvedSourceFile, 51 | }); 52 | } 53 | } 54 | } 55 | 56 | return sourceFileImportsMap; 57 | } 58 | 59 | /** 60 | * https://github.com/dsherret/ts-morph/issues/327 61 | * Note that if the module resolution of the compiler is Classic then it won't resolve those implicit index.ts module specifiers. 62 | * So for example, if the moduleResolution compiler option isn't explicitly set then setting the module 63 | * compiler option to anything but ModuleKind.CommonJS will cause the module resolution kind to resolve to Classic. 64 | * Additionally, if moduleResolution and the module compiler option isn't set, 65 | * then a script target of ES2015 and above will also use Classic module resolution. 66 | */ 67 | private resolveBarrelFileImport( 68 | barrelSourceFile: SourceFile, 69 | name: string, 70 | project: Project, 71 | ): SourceFile | undefined { 72 | // Traverse through export declarations to find the actual source of the named import 73 | for (const exportDeclaration of barrelSourceFile.getExportDeclarations()) { 74 | const exportedSourceFile = 75 | exportDeclaration.getModuleSpecifierSourceFile(); 76 | if (exportedSourceFile == null) continue; 77 | 78 | // Check if the named export is explicitly re-exported 79 | const namedExports = exportDeclaration.getNamedExports(); 80 | if (namedExports.length > 0) { 81 | const matchingExport = namedExports.find((e) => e.getName() === name); 82 | if (matchingExport) { 83 | return exportedSourceFile; 84 | } 85 | } else { 86 | // Handle `export * from ...` case: recursively resolve the export 87 | const schemaVariable = exportedSourceFile.getVariableDeclaration(name); 88 | if (schemaVariable) { 89 | return exportedSourceFile; 90 | } else { 91 | // Continue resolving if it's another barrel file 92 | const baseSourceFile = this.resolveBarrelFileImport( 93 | exportedSourceFile, 94 | name, 95 | project, 96 | ); 97 | if (baseSourceFile) return baseSourceFile; 98 | } 99 | } 100 | } 101 | 102 | return undefined; 103 | } 104 | } 105 | --------------------------------------------------------------------------------
Server side - {response.name}
{description}
Read about NestJS tRPC 1.0 Launch
Discover how to write type-safe end-to-end apis using tRPC in NestJS.
npm install nestjs-trpc
Documentation
It's the best way to write NestJS APIs since the GraphQL adapter!
25 | The client above is not importing any code from the server, only its type declarations. 26 |
110 | 111 |