├── .big ├── client.ts └── server │ ├── context.ts │ └── routers │ └── .gitignore ├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── index.html ├── package.json ├── sandbox.config.json ├── scripts └── generate-big-f-router.ts ├── src ├── @trpc │ ├── client │ │ ├── client.ts │ │ └── index.ts │ └── server │ │ ├── core │ │ ├── initTRPC.ts │ │ ├── middleware.ts │ │ ├── parser.ts │ │ ├── procedure.ts │ │ ├── router.ts │ │ └── utils.ts │ │ ├── error │ │ ├── TRPCError.ts │ │ ├── formatter.ts │ │ └── utils.ts │ │ ├── index.ts │ │ └── rpc │ │ ├── codes.ts │ │ ├── envelopes.ts │ │ ├── index.ts │ │ └── internal │ │ └── invert.ts ├── client.ts ├── server.test.ts └── server │ ├── context.ts │ ├── index.ts │ └── routers │ ├── _app.ts │ ├── mixedRouter.ts │ ├── orgRouter.ts │ └── postRouter.ts ├── tsconfig.json └── yarn.lock /.big/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '../src/@trpc/client'; 2 | import type { appRouter } from './server/routers/_app'; 3 | 4 | const client = createClient(); 5 | 6 | async function main() { 7 | const result = await client.query.r0q0({ input: { hello: 'world' } }); 8 | 9 | console.log(result); 10 | } 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /.big/server/context.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '../../src/@trpc/server'; 2 | 3 | ////////////////////// app //////////////////////////// 4 | // context 5 | type Context = { 6 | user?: { 7 | id: string; 8 | }; 9 | }; 10 | 11 | export const trpc = initTRPC(); 12 | -------------------------------------------------------------------------------- /.big/server/routers/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | test: 5 | name: Build and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['14.x', '16.x'] 11 | os: [ubuntu-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - run: yarn tsc --noEmit 26 | - run: yarn tsc --noEmit --extendedDiagnostics .big/client.ts 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Playground link:** https://stackblitz.com/github/trpc/v10-playground?file=src%2Fserver%2Findex.ts,src%2Fclient.ts,src%2Fserver%2Frouters%2FpostRouter.ts&view=editor 2 | > 3 | > Repo: https://github.com/KATT/trpc-procedure-play 4 | 5 | # [tRPC](https://trpc.io) V10 play 6 | 7 | Draft of how a future [tRPC](https://trpc.io)-version could look like. 8 | 9 | ## Play with it! 10 | 11 | > Do not try to run the project - there's no code implemented, only TypeScript ergonomics. 12 | 13 | 1. Go to `src/server.ts` in CodeSandbox 14 | 2. Try adding/removing/changing queries and mutations. 15 | 3. Go to `src/client.ts` and play around 16 | 17 | ### Big router performance testing 18 | 19 | - Run `yarn codegen` (modify `./scripts/generate-big-f-router.ts` if you want) 20 | - Play with `./big/client.ts` and the connected routers to see how long it takes to get feedback 21 | - Potentially run `yarn tsc --noEmit --extendedDiagnostics --watch .big/client.ts` on the side 22 | 23 | ## Goals & features 24 | 25 | - **More ergonomic API for creating procedures** and building out your backend 26 | - **CMD+Click** from a your frontend and jump straight into the backend procedure. This will work with `react-query` as well! 27 | - **Enabling having a watchers**-based structure - as you see, that `createRouter()` could easily be automatically generated from a file/folder structure. 28 | - **Better scaling** than current structure - the TypeScript server starts choking a bit when you get close to 100 procedures in your backend 29 | - ~**Infer expected errors** as well as data - unsure if this is useful yet or if it'll make it, but pretty sure it'll be nice to have.~ Skipped this because of it's complexity - it can still be added later. 30 | 31 | ## New router API! 32 | 33 | ### §1 Basics 34 | 35 | 36 | #### §1.0 Setting up tRPC 37 | 38 | ```tsx 39 | type Context = { 40 | user?: { 41 | id: string; 42 | memberships: { 43 | organizationId: string; 44 | }[]; 45 | }; 46 | }; 47 | 48 | const trpc = initTRPC(); 49 | 50 | const { 51 | /** 52 | * Builder object for creating procedures 53 | */ 54 | procedure, 55 | /** 56 | * Create reusable middlewares 57 | */ 58 | middleware, 59 | /** 60 | * Create a router 61 | */ 62 | router, 63 | /** 64 | * Merge Routers 65 | */ 66 | mergeRouters, 67 | } = trpc; 68 | ``` 69 | 70 | #### §1.1 Creating a router 71 | 72 | ```tsx 73 | export const appRouter = trpc.router({ 74 | queries: { 75 | // [...] 76 | }, 77 | mutations: { 78 | // [...] 79 | }, 80 | }) 81 | ``` 82 | 83 | #### §1.2 Defining a procedure 84 | 85 | ```tsx 86 | export const appRouter = trpc.router({ 87 | queries: { 88 | // simple procedure without args avialable at postAll` 89 | postList: procedure.resolve(() => postsDb), 90 | } 91 | }); 92 | ``` 93 | 94 | ##### Details about the procedure builder 95 | 96 | Simplified to be more readable - see full implementation in https://github.com/trpc/v10-playground/blob/katt/procedure-chains/src/trpc/server/procedure.ts 97 | 98 | ```tsx 99 | 100 | interface ProcedureBuilder { 101 | /** 102 | * Add an input parser to the procedure. 103 | */ 104 | input( 105 | schema: $TParser, 106 | ): ProcedureBuilder; 107 | /** 108 | * Add an output parser to the procedure. 109 | */ 110 | output( 111 | schema: $TParser, 112 | ): ProcedureBuilder; 113 | /** 114 | * Add a middleware to the procedure. 115 | */ 116 | use( 117 | fn: MiddlewareFunction, 118 | ): ProcedureBuilder 119 | /** 120 | * Extend the procedure with another procedure 121 | */ 122 | concat( 123 | proc: ProcedureBuilder, 124 | ): ProcedureBuilder; 125 | resolve( 126 | resolver: ( 127 | opts: ResolveOptions, 128 | ) => $TOutput, 129 | ): Procedure; 130 | } 131 | ``` 132 | 133 | #### §1.3 Adding input parser 134 | 135 | > Note that I'll skip the `trpc.router({ queries: /*...*/})` below here 136 | 137 | ```tsx 138 | 139 | // get post by id or 404 if it's not found 140 | const postById = procedure 141 | .input( 142 | z.object({ 143 | id: z.string(), 144 | }), 145 | ) 146 | .resolve(({ input }) => { 147 | const post = postsDb.find((post) => post.id === input.id); 148 | if (!post) { 149 | throw new Error('NOT_FOUND'); 150 | } 151 | return { 152 | data: postsDb, 153 | }; 154 | }); 155 | ``` 156 | 157 | #### §1.4 Procedure with middleware 158 | 159 | ```tsx 160 | const whoami = procedure 161 | .use((params) => { 162 | if (!params.ctx.user) { 163 | throw new Error('UNAUTHORIZED'); 164 | } 165 | return params.next({ 166 | ctx: { 167 | // User is now set on the ctx object 168 | user: params.ctx.user, 169 | }, 170 | }); 171 | }) 172 | .resolve(({ ctx }) => { 173 | // `isAuthed()` will propagate new `ctx` 174 | // `ctx.user` is now `NonNullable` 175 | return `your id is ${ctx.user.id}`; 176 | }); 177 | 178 | ``` 179 | 180 | ### §2 Intermediate 🍿 181 | 182 | #### §2.1 Define a reusable middleware 183 | 184 | ```tsx 185 | 186 | const isAuthed = trpc.middleware((params) => { 187 | if (!params.ctx.user) { 188 | throw new Error('zup'); 189 | } 190 | return params.next({ 191 | ctx: { 192 | user: params.ctx.user, 193 | }, 194 | }); 195 | }); 196 | 197 | // Use in procedure: 198 | const whoami = procedure 199 | .use(isAuthed) 200 | .resolve(({ ctx }) => { 201 | // `isAuthed()` will propagate new `ctx` 202 | // `ctx.user` is now `NonNullable` 203 | return `your id is ${ctx.user.id}`; 204 | }); 205 | ``` 206 | 207 | 208 | #### §2.2 Create a bunch of procedures that are all protected 209 | 210 | ```tsx 211 | const protectedProcedure = procedure.use(isAuthed); 212 | 213 | export const appRouter = trpc.router({ 214 | queries: { 215 | postList: protectedProcedure.resolve(() => postsDb), 216 | postById: protectedProcedure 217 | .input( 218 | z.object({ 219 | id: z.string(), 220 | }), 221 | ) 222 | .resolve(({ input }) => { 223 | const post = postsDb.find((post) => post.id === input.id); 224 | if (!post) { 225 | throw new Error('NOT_FOUND'); 226 | } 227 | return { 228 | data: postsDb, 229 | }; 230 | }) 231 | } 232 | }) 233 | ``` 234 | 235 | #### §2.3 Define an `output` schema 236 | 237 | 238 | 239 | ```tsx 240 | procedure 241 | .output(z.void()) 242 | // This will fail because we've explicitly said this procedure is `void` 243 | .resolve(({ input }) => { 244 | return'hello'; 245 | }) 246 | ``` 247 | 248 | #### §2.4 Merging routers 249 | 250 | ```ts 251 | const postRouter = trpc.router({ 252 | queries: { 253 | postList: protectedProcedure.resolve(() => postsDb), 254 | postById: protectedProcedure 255 | .input( 256 | z.object({ 257 | id: z.string(), 258 | }), 259 | ) 260 | .resolve(({ input }) => { 261 | const post = postsDb.find((post) => post.id === input.id); 262 | if (!post) { 263 | throw new Error('NOT_FOUND'); 264 | } 265 | return { 266 | data: postsDb, 267 | }; 268 | }) 269 | } 270 | }) 271 | 272 | const health = trpc.router({ 273 | query: { 274 | healthz: trpc.resolve(() => 'I am alive') 275 | } 276 | }) 277 | 278 | export const appRouter = trpc.mergeRouters( 279 | postRouter, 280 | health 281 | ); 282 | ``` 283 | 284 | 285 | ### §3 Advanced 🧙 286 | 287 | #### Compose dynamic combos of middlewares/input parsers 288 | 289 | ```tsx 290 | 291 | /** 292 | * A reusable combination of an input + middleware that can be reused. 293 | * Accepts a Zod-schema as a generic. 294 | */ 295 | function isPartOfOrg< 296 | TSchema extends z.ZodObject<{ organizationId: z.ZodString }>, 297 | >(schema: TSchema) { 298 | return procedure.input(schema).use((params) => { 299 | const { ctx, input } = params; 300 | const { user } = ctx; 301 | if (!user) { 302 | throw new Error('UNAUTHORIZED'); 303 | } 304 | 305 | if ( 306 | !user.memberships.some( 307 | (membership) => membership.organizationId !== input.organizationId, 308 | ) 309 | ) { 310 | throw new Error('FORBIDDEN'); 311 | } 312 | 313 | return params.next({ 314 | ctx: { 315 | user, 316 | }, 317 | }); 318 | }); 319 | } 320 | 321 | 322 | 323 | const editOrganization = procedure 324 | .concat( 325 | isPartOfOrg( 326 | z.object({ 327 | organizationId: z.string(), 328 | data: z.object({ 329 | name: z.string(), 330 | }), 331 | }), 332 | ), 333 | ) 334 | .resolve(({ ctx, input }) => { 335 | // - User is guaranteed to be part of the organization queried 336 | // - `input` is of type: 337 | // { 338 | // data: { 339 | // name: string; 340 | // }; 341 | // organizationId: string; 342 | // } 343 | 344 | // [.... insert logic here] 345 | }); 346 | ``` 347 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | you're not meant to view this or to run it, just open the src/ and play with it instead. 3 | s.length) 44 | .optional() 45 | .default(''), 46 | }) 47 | ) 48 | .resolve((params) => { 49 | return { 50 | input: params.input, 51 | } 52 | }), 53 | `.trim(), 54 | ); 55 | } 56 | // write router file 57 | const contents = WRAPPER.replace('__CONTENT__', routerFile.join('\n')) 58 | .replace('__IMPORTS__', '') 59 | .replace('__ROUTER_NAME__', `router${routerIndex}`); 60 | fs.writeFileSync(SERVER_DIR + `/router${routerIndex}.ts`, contents); 61 | } 62 | // write `_app.ts` index file that combines all the routers 63 | const imports = new Array(NUM_ROUTERS) 64 | .fill('') 65 | .map((_, index) => `import { router${index} } from './router${index}';`) 66 | .join('\n'); 67 | const content = new Array(NUM_ROUTERS) 68 | .fill('') 69 | .map((_, index) => `...router${index}.queries,`) 70 | .join('\n'); 71 | let indexFile = WRAPPER.replace('__IMPORTS__', imports) 72 | .replace('__ROUTER_NAME__', `appRouter`) 73 | .replace('__CONTENT__', content); 74 | 75 | fs.writeFileSync(SERVER_DIR + '/_app.ts', indexFile); 76 | -------------------------------------------------------------------------------- /src/@trpc/client/client.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from '../server'; 2 | 3 | function createRouterProxy>() { 4 | return new Proxy({} as any, { 5 | get(_, type: string) { 6 | return new Proxy({} as any, { 7 | get(_, path: string) { 8 | return type + '.' + path; 9 | }, 10 | }); 11 | }, 12 | }) as any as TRouter; 13 | } 14 | 15 | export interface TRPCClient> { 16 | query: NonNullable; 17 | mutation: NonNullable; 18 | $query: never; // Observable version of query? 19 | } 20 | 21 | export function createClient< 22 | TRouter extends Router, 23 | >(): TRPCClient { 24 | const proxy = createRouterProxy(); 25 | 26 | return proxy as any; 27 | } 28 | -------------------------------------------------------------------------------- /src/@trpc/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/@trpc/server/core/initTRPC.ts: -------------------------------------------------------------------------------- 1 | import { createRouterWithContext, mergeRouters } from './router'; 2 | import { createBuilder as createProcedure } from './procedure'; 3 | import { createMiddlewareFactory } from './middleware'; 4 | 5 | export function initTRPC< 6 | TContext, 7 | TMeta extends Record = {}, 8 | >() { 9 | return { 10 | /** 11 | * Builder object for creating procedures 12 | */ 13 | procedure: createProcedure(), 14 | /** 15 | * Create reusable middlewares 16 | */ 17 | middleware: createMiddlewareFactory(), 18 | /** 19 | * Create a router 20 | */ 21 | router: createRouterWithContext(), 22 | /** 23 | * Merge Routers 24 | */ 25 | mergeRouters, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/@trpc/server/core/middleware.ts: -------------------------------------------------------------------------------- 1 | import { Params } from './utils'; 2 | import { ProcedureType } from './utils'; 3 | import { MiddlewareMarker } from './utils'; 4 | 5 | /** 6 | * @internal 7 | */ 8 | interface MiddlewareResultBase { 9 | /** 10 | * All middlewares should pass through their `next()`'s output. 11 | * Requiring this marker makes sure that can't be forgotten at compile-time. 12 | */ 13 | readonly marker: MiddlewareMarker; 14 | } 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface MiddlewareOKResult<_TParams extends Params> 20 | extends MiddlewareResultBase { 21 | ok: true; 22 | data: unknown; 23 | // this could be extended with `input`/`rawInput` later 24 | } 25 | 26 | /** 27 | * @internal 28 | */ 29 | interface MiddlewareErrorResult<_TParams extends Params> 30 | extends MiddlewareResultBase { 31 | ok: false; 32 | error: Error; 33 | // we could guarantee it's always of this type 34 | } 35 | 36 | /** 37 | * @internal 38 | */ 39 | export type MiddlewareResult = 40 | | MiddlewareOKResult 41 | | MiddlewareErrorResult; 42 | 43 | /** 44 | * @internal 45 | */ 46 | export type MiddlewareFunction< 47 | TParams extends Params, 48 | TParamsAfter extends Params, 49 | > = (opts: { 50 | ctx: TParams['_ctx_out']; 51 | type: ProcedureType; 52 | path: string; 53 | input: TParams['_input_out']; 54 | rawInput: unknown; 55 | meta: TParams['_meta']; 56 | next: { 57 | (): Promise>; 58 | <$TContext>(opts: { ctx: $TContext }): Promise< 59 | MiddlewareResult<{ 60 | _ctx_in: TParams['_ctx_in']; 61 | _ctx_out: $TContext; 62 | _input_in: TParams['_input_in']; 63 | _input_out: TParams['_input_out']; 64 | _output_in: TParams['_output_in']; 65 | _output_out: TParams['_output_out']; 66 | _meta: TParams['_meta']; 67 | }> 68 | >; 69 | }; 70 | }) => Promise>; 71 | 72 | /** 73 | * @internal 74 | */ 75 | export function createMiddlewareFactory() { 76 | return function createMiddleware<$TNewParams extends Params>( 77 | fn: MiddlewareFunction< 78 | { 79 | _ctx_in: TContext; 80 | _ctx_out: TContext; 81 | _input_out: unknown; 82 | _input_in: unknown; 83 | _output_in: unknown; 84 | _output_out: unknown; 85 | _meta: TMeta; 86 | }, 87 | $TNewParams 88 | >, 89 | ) { 90 | return fn; 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/@trpc/server/core/parser.ts: -------------------------------------------------------------------------------- 1 | export type ParserZodEsque = { 2 | _input: TInput; 3 | _output: TParsedInput; 4 | }; 5 | 6 | export type ParserMyZodEsque = { 7 | parse: (input: any) => TInput; 8 | }; 9 | 10 | export type ParserSuperstructEsque = { 11 | create: (input: unknown) => TInput; 12 | }; 13 | 14 | export type ParserCustomValidatorEsque = ( 15 | input: unknown, 16 | ) => TInput | Promise; 17 | 18 | export type ParserYupEsque = { 19 | validateSync: (input: unknown) => TInput; 20 | }; 21 | export type ParserWithoutInput = 22 | | ParserYupEsque 23 | | ParserSuperstructEsque 24 | | ParserCustomValidatorEsque 25 | | ParserMyZodEsque; 26 | 27 | export type ParserWithInputOutput = ParserZodEsque< 28 | TInput, 29 | TParsedInput 30 | >; 31 | 32 | export type Parser = ParserWithoutInput | ParserWithInputOutput; 33 | 34 | export type inferParser = 35 | TParser extends ParserWithInputOutput 36 | ? { 37 | in: $TIn; 38 | out: $TOut; 39 | } 40 | : TParser extends ParserWithoutInput 41 | ? { 42 | in: $InOut; 43 | out: $InOut; 44 | } 45 | : never; 46 | -------------------------------------------------------------------------------- /src/@trpc/server/core/procedure.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFunction } from './middleware'; 2 | import { Params } from './utils'; 3 | import { Parser, inferParser } from './parser'; 4 | import { 5 | DefaultValue as FallbackValue, 6 | Overwrite, 7 | ProcedureMarker, 8 | UnsetMarker, 9 | } from './utils'; 10 | 11 | // type ProcedureBuilder 12 | type MaybePromise = T | Promise; 13 | interface ResolveOptions { 14 | ctx: TParams['_ctx_out']; 15 | input: TParams['_input_out']; 16 | } 17 | 18 | type ClientContext = Record; 19 | type ProcedureOptions = { 20 | context?: ClientContext; 21 | } & TInput; 22 | 23 | export type Procedure = 24 | (TParams['_input_in'] extends UnsetMarker 25 | ? ( 26 | opts?: ProcedureOptions<{ input?: undefined }>, 27 | ) => Promise 28 | : TParams['_input_in'] extends undefined 29 | ? ( 30 | opts?: ProcedureOptions<{ input?: TParams['_input_in'] }>, 31 | ) => Promise 32 | : ( 33 | opts: ProcedureOptions<{ input: TParams['_input_in'] }>, 34 | ) => Promise) & 35 | ProcedureMarker; 36 | 37 | type CreateProcedureReturnInput< 38 | TPrev extends Params, 39 | TNext extends Params, 40 | > = ProcedureBuilder<{ 41 | _meta: TPrev['_meta']; 42 | _ctx_in: TPrev['_ctx_in']; 43 | _ctx_out: Overwrite; 44 | _input_out: FallbackValue; 45 | _input_in: FallbackValue; 46 | _output_in: FallbackValue; 47 | _output_out: FallbackValue; 48 | }>; 49 | 50 | export interface ProcedureBuilder { 51 | /** 52 | * Add an input parser to the procedure. 53 | */ 54 | input<$TParser extends Parser>( 55 | schema: $TParser, 56 | ): ProcedureBuilder<{ 57 | _meta: TParams['_meta']; 58 | _ctx_in: TParams['_ctx_in']; 59 | _ctx_out: TParams['_ctx_out']; 60 | _output_in: TParams['_output_in']; 61 | _output_out: TParams['_output_out']; 62 | _input_in: inferParser<$TParser>['in']; 63 | _input_out: inferParser<$TParser>['out']; 64 | }>; 65 | /** 66 | * Add an output parser to the procedure. 67 | */ 68 | output<$TParser extends Parser>( 69 | schema: $TParser, 70 | ): ProcedureBuilder<{ 71 | _meta: TParams['_meta']; 72 | _ctx_in: TParams['_ctx_in']; 73 | _ctx_out: TParams['_ctx_out']; 74 | _input_in: TParams['_input_in']; 75 | _input_out: TParams['_input_out']; 76 | _output_in: inferParser<$TParser>['in']; 77 | _output_out: inferParser<$TParser>['out']; 78 | }>; 79 | /** 80 | * Add a meta data to the procedure. 81 | */ 82 | meta(meta: TParams['_meta']): ProcedureBuilder; 83 | /** 84 | * Add a middleware to the procedure. 85 | */ 86 | use<$TParams extends Params>( 87 | fn: MiddlewareFunction, 88 | ): CreateProcedureReturnInput; 89 | /** 90 | * Extend the procedure with another procedure 91 | */ 92 | concat<$ProcedureReturnInput extends ProcedureBuilder>( 93 | proc: $ProcedureReturnInput, 94 | ): $ProcedureReturnInput extends ProcedureBuilder 95 | ? CreateProcedureReturnInput 96 | : never; 97 | /** 98 | * Resolve the procedure 99 | */ 100 | resolve<$TOutput>( 101 | resolver: ( 102 | opts: ResolveOptions, 103 | ) => MaybePromise>, 104 | ): UnsetMarker extends TParams['_output_out'] 105 | ? Procedure< 106 | Overwrite< 107 | TParams, 108 | { 109 | _output_in: $TOutput; 110 | _output_out: $TOutput; 111 | } 112 | > 113 | > 114 | : Procedure; 115 | } 116 | 117 | export function createBuilder(): ProcedureBuilder<{ 118 | _ctx_in: TContext; 119 | _ctx_out: TContext; 120 | _input_out: UnsetMarker; 121 | _input_in: UnsetMarker; 122 | _output_in: UnsetMarker; 123 | _output_out: UnsetMarker; 124 | _meta: TMeta; 125 | }> { 126 | throw new Error('unimplemented'); 127 | } 128 | -------------------------------------------------------------------------------- /src/@trpc/server/core/router.ts: -------------------------------------------------------------------------------- 1 | import { ErrorFormatter } from '../error/formatter'; 2 | import { Procedure } from './procedure'; 3 | 4 | // FIXME this should properly use TContext 5 | type ProcedureRecord<_TContext> = Record>; 6 | 7 | /** 8 | * @internal 9 | */ 10 | export interface Router { 11 | queries?: ProcedureRecord; 12 | mutations?: ProcedureRecord; 13 | subscriptions?: ProcedureRecord; 14 | errorFormatter?: ErrorFormatter; 15 | } 16 | 17 | /** 18 | * @internal 19 | */ 20 | type ValidateShape = 21 | TActualShape extends TExpectedShape 22 | ? Exclude extends never 23 | ? TActualShape 24 | : TExpectedShape 25 | : never; 26 | 27 | /** 28 | * 29 | * @internal 30 | */ 31 | export function createRouterWithContext() { 32 | return function createRouter>( 33 | procedures: ValidateShape>, 34 | ): TProcedures { 35 | return procedures as any; 36 | }; 37 | } 38 | 39 | type EnsureRecord = undefined extends T ? {} : T; 40 | type PickFirstValid = undefined extends T 41 | ? undefined extends K 42 | ? never 43 | : K 44 | : T; 45 | 46 | type mergeRouters< 47 | TContext, 48 | A extends Router, 49 | B extends Router, 50 | > = { 51 | queries: EnsureRecord & EnsureRecord; 52 | mutations: EnsureRecord & EnsureRecord; 53 | subscriptions: EnsureRecord & 54 | EnsureRecord; 55 | errorFormatter: PickFirstValid; 56 | }; 57 | 58 | type mergeRoutersVariadic[]> = Routers extends [] 59 | ? {} 60 | : Routers extends [infer First, ...infer Rest] 61 | ? First extends Router 62 | ? Rest extends Router[] 63 | ? mergeRouters> 64 | : never 65 | : never 66 | : never; 67 | 68 | export function mergeRouters[]>( 69 | ..._routers: TRouters 70 | ): mergeRoutersVariadic { 71 | throw new Error('Unimplemnted'); 72 | } 73 | -------------------------------------------------------------------------------- /src/@trpc/server/core/utils.ts: -------------------------------------------------------------------------------- 1 | export type ProcedureType = 'query' | 'mutation' | 'subscription'; 2 | 3 | /** 4 | * @internal 5 | */ 6 | export type Overwrite = Omit & U; 7 | /** 8 | * @internal 9 | */ 10 | export type DefaultValue = UnsetMarker extends TValue 11 | ? TFallback 12 | : TValue; 13 | 14 | /** 15 | * @internal 16 | */ 17 | export const middlewareMarker = Symbol('middlewareMarker'); 18 | /** 19 | * @internal 20 | */ 21 | export type MiddlewareMarker = typeof middlewareMarker; 22 | 23 | /** 24 | * @internal 25 | */ 26 | export const unsetMarker = Symbol('unsetMarker'); 27 | /** 28 | * @internal 29 | */ 30 | export type UnsetMarker = typeof unsetMarker; 31 | 32 | /** 33 | * @internal 34 | */ 35 | export const procedureMarker = Symbol('procedureMarker'); 36 | /** 37 | * @internal 38 | */ 39 | export type ProcedureMarker = typeof procedureMarker; 40 | 41 | /** 42 | * @internal 43 | */ 44 | export interface Params< 45 | TContextIn = unknown, 46 | TContextOut = unknown, 47 | TInputIn = unknown, 48 | TInputOut = unknown, 49 | TOutputIn = unknown, 50 | TOutputOut = unknown, 51 | TMeta = unknown, 52 | > { 53 | /** 54 | * @internal 55 | */ 56 | _meta: TMeta; 57 | /** 58 | * @internal 59 | */ 60 | _ctx_in: TContextIn; 61 | /** 62 | * @internal 63 | */ 64 | _ctx_out: TContextOut; 65 | /** 66 | * @internal 67 | */ 68 | _output_in: TOutputIn; 69 | /** 70 | * @internal 71 | */ 72 | _output_out: TOutputOut; 73 | /** 74 | * @internal 75 | */ 76 | _input_in: TInputIn; 77 | /** 78 | * @internal 79 | */ 80 | _input_out: TInputOut; 81 | } 82 | -------------------------------------------------------------------------------- /src/@trpc/server/error/TRPCError.ts: -------------------------------------------------------------------------------- 1 | import { getMessageFromUnkownError } from './utils'; 2 | import { TRPC_ERROR_CODE_KEY } from '../rpc/codes'; 3 | 4 | export class TRPCError extends Error { 5 | public readonly cause?: Error | undefined; 6 | public readonly code; 7 | 8 | constructor(opts: { 9 | message?: string; 10 | code: TRPC_ERROR_CODE_KEY; 11 | cause?: Error; 12 | }) { 13 | const cause = opts.cause; 14 | const code = opts.code; 15 | const message = opts.message ?? getMessageFromUnkownError(cause, code); 16 | 17 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 18 | // @ts-ignore https://github.com/tc39/proposal-error-cause 19 | super(message, { cause }); 20 | 21 | this.code = code; 22 | this.cause = cause; 23 | this.name = 'TRPCError'; 24 | 25 | Object.setPrototypeOf(this, new.target.prototype); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/@trpc/server/error/formatter.ts: -------------------------------------------------------------------------------- 1 | import { ProcedureType } from '../core/utils'; 2 | import { 3 | TRPCErrorShape, 4 | TRPC_ERROR_CODE_KEY, 5 | TRPC_ERROR_CODE_NUMBER, 6 | } from '../rpc'; 7 | import { TRPCError } from './TRPCError'; 8 | 9 | /** 10 | * @internal 11 | */ 12 | export type ErrorFormatter> = ({ 13 | error, 14 | }: { 15 | error: TRPCError; 16 | type: ProcedureType | 'unknown'; 17 | path: string | undefined; 18 | input: unknown; 19 | ctx: undefined | TContext; 20 | shape: DefaultErrorShape; 21 | }) => TShape; 22 | 23 | /** 24 | * @internal 25 | */ 26 | export type DefaultErrorData = { 27 | code: TRPC_ERROR_CODE_KEY; 28 | httpStatus: number; 29 | path?: string; 30 | stack?: string; 31 | }; 32 | 33 | /** 34 | * @internal 35 | */ 36 | export interface DefaultErrorShape 37 | extends TRPCErrorShape { 38 | message: string; 39 | code: TRPC_ERROR_CODE_NUMBER; 40 | } 41 | 42 | export const defaultFormatter: ErrorFormatter = ({ 43 | shape, 44 | }: { 45 | shape: DefaultErrorShape; 46 | }) => { 47 | return shape; 48 | }; 49 | -------------------------------------------------------------------------------- /src/@trpc/server/error/utils.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from './TRPCError'; 2 | 3 | export function getMessageFromUnkownError( 4 | err: unknown, 5 | fallback: string, 6 | ): string { 7 | if (typeof err === 'string') { 8 | return err; 9 | } 10 | 11 | if (err instanceof Error && typeof err.message === 'string') { 12 | return err.message; 13 | } 14 | return fallback; 15 | } 16 | 17 | export function getErrorFromUnknown(cause: unknown): TRPCError { 18 | // this should ideally be an `instanceof TRPCError` but for some reason that isn't working 19 | // ref https://github.com/trpc/trpc/issues/331 20 | if (cause instanceof Error && cause.name === 'TRPCError') { 21 | return cause as TRPCError; 22 | } 23 | 24 | let errorCause: Error | undefined = undefined; 25 | let stack: string | undefined = undefined; 26 | 27 | if (cause instanceof Error) { 28 | errorCause = cause; 29 | // take stack trace from cause 30 | stack = cause.stack; 31 | } 32 | 33 | const err = new TRPCError({ 34 | code: 'INTERNAL_SERVER_ERROR', 35 | cause: errorCause, 36 | }); 37 | 38 | err.stack = stack; 39 | 40 | return err; 41 | } 42 | 43 | export function getCauseFromUnknown(cause: unknown) { 44 | if (cause instanceof Error) { 45 | return cause; 46 | } 47 | 48 | return undefined; 49 | } 50 | -------------------------------------------------------------------------------- /src/@trpc/server/index.ts: -------------------------------------------------------------------------------- 1 | export { initTRPC } from './core/initTRPC'; 2 | export { Router } from './core/router'; 3 | export { TRPCError } from './error/TRPCError'; 4 | -------------------------------------------------------------------------------- /src/@trpc/server/rpc/codes.ts: -------------------------------------------------------------------------------- 1 | import { invert } from './internal/invert'; 2 | 3 | // reference: https://www.jsonrpc.org/specification 4 | 5 | /** 6 | * JSON-RPC 2.0 Error codes 7 | * 8 | * `-32000` to `-32099` are reserved for implementation-defined server-errors. 9 | * For tRPC we're copying the last digits of HTTP 4XX errors. 10 | */ 11 | export const TRPC_ERROR_CODES_BY_KEY = { 12 | /** 13 | * Invalid JSON was received by the server. 14 | * An error occurred on the server while parsing the JSON text. 15 | */ 16 | PARSE_ERROR: -32700, 17 | /** 18 | * The JSON sent is not a valid Request object. 19 | */ 20 | BAD_REQUEST: -32600, // 400 21 | /** 22 | * Internal JSON-RPC error. 23 | */ 24 | INTERNAL_SERVER_ERROR: -32603, 25 | // Implementation specific errors 26 | UNAUTHORIZED: -32001, // 401 27 | FORBIDDEN: -32003, // 403 28 | NOT_FOUND: -32004, // 404 29 | METHOD_NOT_SUPPORTED: -32005, // 405 30 | TIMEOUT: -32008, // 408 31 | CONFLICT: -32009, // 409 32 | PRECONDITION_FAILED: -32012, // 412 33 | PAYLOAD_TOO_LARGE: -32013, // 413 34 | CLIENT_CLOSED_REQUEST: -32099, // 499 35 | } as const; 36 | 37 | export const TRPC_ERROR_CODES_BY_NUMBER = invert(TRPC_ERROR_CODES_BY_KEY); 38 | type ValueOf = T[keyof T]; 39 | 40 | export type TRPC_ERROR_CODE_NUMBER = ValueOf; 41 | export type TRPC_ERROR_CODE_KEY = keyof typeof TRPC_ERROR_CODES_BY_KEY; 42 | -------------------------------------------------------------------------------- /src/@trpc/server/rpc/envelopes.ts: -------------------------------------------------------------------------------- 1 | import { ProcedureType } from '../core/utils'; 2 | import { TRPC_ERROR_CODE_NUMBER } from './codes'; 3 | 4 | type JSONRPC2RequestId = number | string | null; 5 | 6 | /** 7 | * All requests/responses extends this shape 8 | */ 9 | interface JSONRPC2BaseEnvelope { 10 | id: JSONRPC2RequestId; 11 | jsonrpc?: '2.0'; 12 | } 13 | 14 | interface JSONRPC2BaseRequest 15 | extends JSONRPC2BaseEnvelope { 16 | method: TMethod; 17 | } 18 | interface JSONRPC2Request 19 | extends JSONRPC2BaseRequest { 20 | params: TParams; 21 | } 22 | 23 | interface JSONRPC2ResultResponse 24 | extends JSONRPC2BaseEnvelope { 25 | result: TResult; 26 | } 27 | 28 | // inner types 29 | export interface TRPCErrorShape< 30 | TCode extends number = TRPC_ERROR_CODE_NUMBER, 31 | TData extends Record = Record, 32 | > { 33 | code: TCode; 34 | message: string; 35 | data: TData; 36 | } 37 | 38 | /** 39 | * The result data wrapped 40 | */ 41 | export type TRPCResult = 42 | | { 43 | type: 'data'; 44 | data: TData; 45 | } 46 | | { 47 | type: 'started'; 48 | } 49 | | { 50 | type: 'stopped'; 51 | }; 52 | 53 | /** 54 | * Request envelope 55 | */ 56 | export type TRPCRequest = 57 | | JSONRPC2Request<'subscription.stop'> 58 | | JSONRPC2Request< 59 | ProcedureType, 60 | { 61 | path: string; 62 | input: unknown; 63 | } 64 | >; 65 | 66 | /** 67 | * OK response 68 | */ 69 | export type TRPCResultResponse = JSONRPC2ResultResponse< 70 | TRPCResult 71 | >; 72 | 73 | /** 74 | * Generic response wrapper that is either a result or error 75 | */ 76 | export type TRPCResponse< 77 | TData = unknown, 78 | TError extends TRPCErrorShape = TRPCErrorShape, 79 | > = TRPCResultResponse | TRPCErrorResponse; 80 | 81 | /** 82 | * Error response 83 | */ 84 | export interface TRPCErrorResponse< 85 | TError extends TRPCErrorShape = TRPCErrorShape, 86 | > extends JSONRPC2BaseEnvelope { 87 | error: TError; 88 | } 89 | 90 | /** 91 | * The server asked the client to reconnect - useful when restarting/redeploying service 92 | */ 93 | export interface TRPCReconnectNotification 94 | extends JSONRPC2BaseRequest<'reconnect'> { 95 | id: null; 96 | } 97 | 98 | /** 99 | * The client's incoming request types 100 | */ 101 | export type TRPCClientIncomingRequest = 102 | TRPCReconnectNotification /* could be extended in future */; 103 | 104 | /** 105 | * The client's received messages shape 106 | */ 107 | export type TRPCClientIncomingMessage = 108 | | TRPCResponse 109 | | TRPCClientIncomingRequest; 110 | -------------------------------------------------------------------------------- /src/@trpc/server/rpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './codes'; 2 | export * from './envelopes'; 3 | -------------------------------------------------------------------------------- /src/@trpc/server/rpc/internal/invert.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | type KeyFromValue> = { 5 | [K in keyof T]: V extends T[K] ? K : never; 6 | }[keyof T]; 7 | 8 | /** 9 | * @internal 10 | */ 11 | type Invert> = { 12 | [V in T[keyof T]]: KeyFromValue; 13 | }; 14 | 15 | /** 16 | * @internal 17 | */ 18 | export function invert>( 19 | obj: T, 20 | ): Invert { 21 | const newObj = Object.create(null) as any; 22 | for (const key in obj) { 23 | const v = obj[key]; 24 | newObj[v] = key; 25 | } 26 | return newObj; 27 | } 28 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import type { appRouter } from './server'; 2 | import { createClient } from './@trpc/client'; 3 | 4 | const client = createClient(); 5 | 6 | async function main() { 7 | // you can CMD+click `postAll` / `postById` here 8 | const greeting = await client.query.postList(); 9 | 10 | const byId = await client.query.postById({ 11 | input: { id: '1' }, 12 | }); 13 | 14 | console.log('data', { greeting, byId }); 15 | } 16 | 17 | main(); 18 | -------------------------------------------------------------------------------- /src/server.test.ts: -------------------------------------------------------------------------------- 1 | import { expectTypeOf } from 'expect-type'; 2 | import { appRouter } from './server'; 3 | 4 | ///////////// this below are just tests that the type checking is doing it's thing right //////////// 5 | async function main() { 6 | { 7 | // query 'whoami' 8 | const output = await appRouter.queries.viewerWhoAmi(); 9 | console.log({ output }); 10 | expectTypeOf(output).not.toBeAny(); 11 | expectTypeOf(output).toBeString(); 12 | 13 | // should work 14 | await appRouter.queries.viewerWhoAmi({}); 15 | await appRouter.queries.viewerWhoAmi({ 16 | input: undefined, 17 | }); 18 | } 19 | { 20 | const output = await appRouter.mutations.updateToken({ input: 'asd' }); 21 | expectTypeOf(output).toMatchTypeOf(); 22 | } 23 | 24 | { 25 | const output = await appRouter.mutations.editOrg({ 26 | input: { 27 | organizationId: '123', 28 | data: { 29 | name: 'asd', 30 | }, 31 | }, 32 | }); 33 | expectTypeOf(output).toMatchTypeOf<{ 34 | name?: string; 35 | id: string; 36 | }>(); 37 | } 38 | { 39 | const output = await appRouter.mutations.updateToken({ input: 'hey' }); 40 | 41 | expectTypeOf(output).toMatchTypeOf<'ok'>(); 42 | } 43 | { 44 | const output = await appRouter.mutations.postAdd({ 45 | input: { 46 | title: 'asd', 47 | body: 'asd', 48 | }, 49 | }); 50 | 51 | expectTypeOf(output).toMatchTypeOf<{ 52 | id: string; 53 | title: string; 54 | body: string; 55 | userId: string; 56 | }>(); 57 | } 58 | 59 | { 60 | // if you hover result we can see that we can infer both the result and every possible expected error 61 | // const result = await appRouter.queries.greeting({ hello: 'there' }); 62 | // if ('error' in result && result.error) { 63 | // if ('zod' in result.error) { 64 | // // zod error inferred - useful for forms w/o libs 65 | // console.log(result.error.zod.hello?._errors); 66 | // } 67 | // } else { 68 | // console.log(result); 69 | // } 70 | // // some type testing below 71 | // type MyProcedure = inferProcedure; 72 | // expectTypeOf().toMatchTypeOf<{ 73 | // user?: { id: string }; 74 | // }>(); 75 | // expectTypeOf().toMatchTypeOf<{ 76 | // greeting: string; 77 | // }>(); 78 | // expectTypeOf().toMatchTypeOf<{ 79 | // hello: string; 80 | // lengthOf?: string; 81 | // }>(); 82 | // expectTypeOf().toMatchTypeOf<{ 83 | // hello: string; 84 | // lengthOf: number; 85 | // }>(); 86 | } 87 | // { 88 | // // no leaky 89 | // const trpc = initTRPC(); 90 | // trpc.router({ 91 | // queries: { 92 | // test: trpc.resolver(() => { 93 | // return 'ok'; 94 | // }), 95 | // }, 96 | // // @ts-expect-error should error 97 | // doesNotExist: {}, 98 | // }); 99 | // } 100 | // { 101 | // const result = await appRouter.mutations['fireAndForget']('hey'); 102 | // console.log(result); 103 | // } 104 | } 105 | 106 | main(); 107 | -------------------------------------------------------------------------------- /src/server/context.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '../@trpc/server'; 2 | 3 | ////////// app bootstrap & middlewares //////// 4 | export type Context = { 5 | db?: {}; 6 | user?: { 7 | id: string; 8 | memberships: { 9 | organizationId: string; 10 | }[]; 11 | }; 12 | }; 13 | export const trpc = initTRPC(); 14 | 15 | export const procedure = trpc.procedure; 16 | 17 | /** 18 | * Middleware that checks if the user is logged in. 19 | */ 20 | export const isAuthed = trpc.middleware((params) => { 21 | if (!params.ctx.user) { 22 | throw new Error('zup'); 23 | } 24 | return params.next({ 25 | ctx: { 26 | user: params.ctx.user, 27 | }, 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | export { appRouter } from './routers/_app'; 2 | -------------------------------------------------------------------------------- /src/server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { trpc } from '../context'; 2 | import { postRouter } from './postRouter'; 3 | import { mixedRouter } from './mixedRouter'; 4 | import { orgRouter } from './orgRouter'; 5 | import { ZodError } from 'zod'; 6 | 7 | const formatErrors = trpc.router({ 8 | errorFormatter({ error, shape }) { 9 | if (error.cause instanceof ZodError) { 10 | return { 11 | ...shape, 12 | data: { 13 | ...shape.data, 14 | type: 'zod' as const, 15 | errors: error.cause.errors, 16 | }, 17 | }; 18 | } 19 | return shape; 20 | }, 21 | }); 22 | 23 | /** 24 | * Create the app's router based on the mixedRouter and postRouter. 25 | */ 26 | export const appRouter = trpc.mergeRouters( 27 | formatErrors, 28 | mixedRouter, 29 | postRouter, 30 | orgRouter, 31 | ); 32 | -------------------------------------------------------------------------------- /src/server/routers/mixedRouter.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { isAuthed, procedure, trpc } from '../context'; 3 | 4 | // Router with some mixed procedures 5 | export const mixedRouter = trpc.router({ 6 | queries: { 7 | // procedure with input validation called `greeting` 8 | greeting: procedure 9 | .input( 10 | z.object({ 11 | hello: z.string(), 12 | lengthOf: z 13 | .string() 14 | .transform((s) => s.length) 15 | .optional() 16 | .default(''), 17 | }), 18 | ) 19 | .resolve((params) => { 20 | return { 21 | greeting: 'hello ' + params.ctx.user?.id ?? params.input.hello, 22 | }; 23 | }), 24 | // procedure with auth 25 | viewerWhoAmi: procedure.use(isAuthed).resolve(({ ctx }) => { 26 | // `isAuthed()` will propagate new `ctx` 27 | // `ctx.user` is now `NonNullable` 28 | return `your id is ${ctx.user.id}`; 29 | }), 30 | }, 31 | 32 | mutations: { 33 | fireAndForget: procedure.input(z.string()).resolve(() => { 34 | // no return 35 | }), 36 | 37 | updateTokenHappy: procedure 38 | .input(z.string()) 39 | .output(z.literal('ok')) 40 | .resolve(() => { 41 | return 'ok'; 42 | }), 43 | updateToken: procedure 44 | .input(z.string()) 45 | .output(z.literal('ok')) 46 | // @ts-expect-error output validation 47 | .resolve(({ input }) => { 48 | return input; 49 | }), 50 | 51 | voidResponse: procedure 52 | .output(z.void()) 53 | // @ts-expect-error output validation 54 | .resolve(({ input }) => { 55 | return input; 56 | }), 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /src/server/routers/orgRouter.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { TRPCError } from '../../@trpc/server'; 3 | import { procedure, trpc } from '../context'; 4 | 5 | /** 6 | * A reusable combination of an input + middleware that can be reused. 7 | * Accepts a Zod-schema as a generic. 8 | */ 9 | function isPartofOrg< 10 | TSchema extends z.ZodObject<{ organizationId: z.ZodString }>, 11 | >(schema: TSchema) { 12 | return procedure.input(schema).use((params) => { 13 | const { ctx, input } = params; 14 | const { user } = ctx; 15 | if (!user) { 16 | throw new TRPCError({ code: 'UNAUTHORIZED' }); 17 | } 18 | 19 | if ( 20 | !user.memberships.some( 21 | (membership) => membership.organizationId !== input.organizationId, 22 | ) 23 | ) { 24 | throw new TRPCError({ code: 'FORBIDDEN' }); 25 | } 26 | 27 | return params.next({ 28 | ctx: { 29 | user, 30 | }, 31 | }); 32 | }); 33 | } 34 | // Router with some mixed procedures 35 | export const orgRouter = trpc.router({ 36 | mutations: { 37 | editOrg: procedure 38 | .concat( 39 | isPartofOrg( 40 | z.object({ 41 | organizationId: z.string(), 42 | data: z 43 | .object({ 44 | name: z.string(), 45 | }) 46 | .partial(), 47 | }), 48 | ), 49 | ) 50 | .resolve(({ input }) => { 51 | // - User is guaranteed to be part of the organization queried 52 | //- `input` is of type: 53 | // { 54 | // data: { 55 | // name: string; 56 | // }; 57 | // organizationId: string; 58 | // } 59 | // [.... logic] 60 | return { 61 | id: input.organizationId, 62 | ...input.data, 63 | }; 64 | }), 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /src/server/routers/postRouter.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { isAuthed, procedure, trpc } from '../context'; 3 | 4 | // mock db 5 | export let postsDb = [ 6 | { 7 | id: '1', 8 | title: 'hello tRPC', 9 | body: 'this is a preview of v10', 10 | userId: 'KATT', 11 | }, 12 | ]; 13 | 14 | // Router regarding posts 15 | 16 | export const postRouter = trpc.router({ 17 | queries: { 18 | // simple procedure without args avialable at postAll` 19 | postList: procedure.resolve(() => postsDb), 20 | // get post by id or 404 if it's not found 21 | postById: procedure 22 | .input( 23 | z.object({ 24 | id: z.string(), 25 | }), 26 | ) 27 | .resolve(({ input }) => { 28 | const post = postsDb.find((post) => post.id === input.id); 29 | if (!post) { 30 | throw new Error('NOT_FOUND'); 31 | } 32 | return post; 33 | }), 34 | }, 35 | mutations: { 36 | // mutation with auth + input 37 | postAdd: procedure 38 | .input( 39 | z.object({ 40 | title: z.string(), 41 | body: z.string(), 42 | }), 43 | ) 44 | .use(isAuthed) 45 | .resolve(({ ctx, input }) => { 46 | const post: typeof postsDb[number] = { 47 | ...input, 48 | id: `${Math.random()}`, 49 | userId: ctx.user.id, 50 | }; 51 | postsDb.push(post); 52 | return post; 53 | }), 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "lib": ["ES2019", "DOM"], 5 | "module": "commonjs", 6 | "target": "ES2019", 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "strictNullChecks": true, 11 | "strictFunctionTypes": true, 12 | "strictBindCallApply": true, 13 | "strictPropertyInitialization": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "exclude": ["node_modules"], 20 | "include": ["src", ".big"] 21 | } 22 | --------------------------------------------------------------------------------