Skip to content
tRPC

NestJS

Serve a contract from Nest controllers with @trpc12/nest.

@trpc12/nest serves a contract from Nest controllers. @experimental_Implement(contract) adds the Nest route for the contract's REST mapping; the method returns the procedure, so the controller's injected services reach the resolver through this. The package is experimental, so its values are named experimental_* until it's promoted.

import { function experimental_Implement(contract: object): (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => voidexperimental_Implement, class experimental_TrpcModuleexperimental_TrpcModule } from '@trpc12/nest';

const 
const impl: ImplementResult<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, {
    post: {
        byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
            meta: {
                route?: Route | undefined;
            };
        }>, {
            inputIn: {
                id: string;
            };
            inputOut: {
                id: string;
            };
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                ... 4 more ...;
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            requires: never;
        }>, {
            ...;
        }>, {
            ...;
        }>>;
    };
}>
impl
=
const t: TrpcRoot<{
    user: User | null;
}, {
    route?: Route | undefined;
}, ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>>
t
.
TrpcRoot<{ user: User | null; }, { route?: Route | undefined; }, ExtendedDef<RootDef<{ user: User | null; }, {}>, RootDef<{}, OpenApiMeta>>>.implement<{
    post: {
        byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
            meta: {
                route?: Route | undefined;
            };
        }>, {
            inputIn: {
                id: string;
            };
            inputOut: {
                id: string;
            };
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                readonly maximum: string | number;
                readonly inclusive?: boolean | undefined;
                readonly exact?: boolean | undefined;
                readonly path: readonly [] | readonly [...];
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            requires: never;
        }>, {
            ...;
        }>, {
            ...;
        }>>;
    };
}>(contract: {
    post: {
        byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
            meta: {
                route?: Route | undefined;
            };
        }>, {
            inputIn: {
                id: string;
            };
            inputOut: {
                id: string;
            };
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                readonly maximum: string | number;
                readonly inclusive?: boolean | undefined;
                readonly exact?: boolean | undefined;
                readonly path: readonly [] | readonly [...];
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            requires: never;
        }>, {
            ...;
        }>, {
            ...;
        }>>;
    };
}): ImplementResult<...>
implement
(
const appContract: {
    post: {
        byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
            meta: {
                route?: Route | undefined;
            };
        }>, {
            inputIn: {
                id: string;
            };
            inputOut: {
                id: string;
            };
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                ... 4 more ...;
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            requires: never;
        }>, {
            ...;
        }>, {
            ...;
        }>>;
    };
}
appContract
);
@function Controller(): ClassDecorator (+2 overloads)Controller() class class PostControllerPostController { constructor(@function Inject(token?: InjectionToken | ForwardReference): PropertyDecorator & ParameterDecoratorInject(class PostsPosts) private readonly PostController.posts: Postsposts: class PostsPosts) {} @function experimental_Implement(contract: object): (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => voidexperimental_Implement(
const appContract: {
    post: {
        byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
            meta: {
                route?: Route | undefined;
            };
        }>, {
            inputIn: {
                id: string;
            };
            inputOut: {
                id: string;
            };
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                readonly maximum: string | number;
                readonly inclusive?: boolean | undefined;
                readonly exact?: boolean | undefined;
                readonly path: readonly [] | readonly [...];
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            requires: never;
        }>, {
            ...;
        }>, {
            ...;
        }>>;
    };
}
appContract
.
post: {
    byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
        meta: {
            route?: Route | undefined;
        };
    }>, {
        inputIn: {
            id: string;
        };
        inputOut: {
            id: string;
        };
        errors: InputValidationError<{
            readonly code: "invalid_type";
            readonly expected: z.core.$ZodInvalidTypeExpected;
            readonly path: readonly [] | readonly ["id"];
            readonly message: string;
        } | {
            readonly code: "too_big";
            readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
            readonly maximum: string | number;
            readonly inclusive?: boolean | undefined;
            readonly exact?: boolean | undefined;
            readonly path: readonly [] | readonly [...];
            readonly message: string;
        } | ... 9 more ... | {
            ...;
        }>;
        requires: never;
    }>, {
        ...;
    }>, {
        ...;
    }>>;
}
post
.
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        readonly maximum: string | number;
        readonly inclusive?: boolean | undefined;
        readonly exact?: boolean | undefined;
        readonly path: readonly [] | readonly [...];
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>
byId
) // .meta({ route: { method: 'GET', path: '/posts/{id}' } })
PostController.byId(): ImplementedProcedure<ImplementerDef<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        ... 4 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>, ContractProcedure<...>, never>
byId
() {
return
const impl: ImplementResult<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, {
    post: {
        byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
            meta: {
                route?: Route | undefined;
            };
        }>, {
            inputIn: {
                id: string;
            };
            inputOut: {
                id: string;
            };
            errors: InputValidationError<{
                readonly code: "invalid_type";
                readonly expected: z.core.$ZodInvalidTypeExpected;
                readonly path: readonly [] | readonly ["id"];
                readonly message: string;
            } | {
                readonly code: "too_big";
                readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
                readonly maximum: string | number;
                readonly inclusive?: boolean | undefined;
                readonly exact?: boolean | undefined;
                readonly path: readonly [] | readonly [...];
                readonly message: string;
            } | ... 9 more ... | {
                ...;
            }>;
            requires: never;
        }>, {
            ...;
        }>, {
            ...;
        }>>;
    };
}>
impl
.
post: {
    readonly byId: ProcedureImplementer<ImplementerDef<ExtendedDef<RootDef<{
        user: User | null;
    }, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
        meta: {
            route?: Route | undefined;
        };
    }>, {
        inputIn: {
            id: string;
        };
        inputOut: {
            id: string;
        };
        errors: InputValidationError<{
            readonly code: "invalid_type";
            readonly expected: z.core.$ZodInvalidTypeExpected;
            readonly path: readonly [] | readonly ["id"];
            readonly message: string;
        } | {
            readonly code: "too_big";
            readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
            readonly maximum: string | number;
            readonly inclusive?: boolean | undefined;
            readonly exact?: boolean | undefined;
            readonly path: readonly [] | readonly [...];
            readonly message: string;
        } | ... 9 more ... | {
            ...;
        }>;
        requires: never;
    }>, {
        ...;
    }>, {
        ...;
    }>>, ContractProcedure<...>>;
}
post
.
byId: ProcedureImplementer<ImplementerDef<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        readonly maximum: string | number;
        readonly inclusive?: boolean | undefined;
        readonly exact?: boolean | undefined;
        readonly path: readonly [] | readonly [...];
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>, ContractProcedure<...>>
byId
.
query: <TrpcError<"NOT_FOUND", {
    id: string;
}> | Post>(resolver: (opts: ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        ... 5 more ...;
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>>) => (TrpcError<...> | Post) & (MaybePromise<...> | Effect<...>)) => ImplementedProcedure<...>
query
(
(
opts: ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        readonly maximum: string | number;
        readonly inclusive?: boolean | undefined;
        readonly exact?: boolean | undefined;
        readonly path: readonly [] | readonly [...];
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>>
opts
) =>
this.PostController.posts: Postsposts.Posts.find(id: string): Post | undefinedfind(
opts: ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        readonly maximum: string | number;
        readonly inclusive?: boolean | undefined;
        readonly exact?: boolean | undefined;
        readonly path: readonly [] | readonly [...];
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>>
opts
.
ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{ user: User | null; }, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, { ...; }>, { ...; }>, { ...; }>, { ...; }>>>.input: {
    id: string;
}
input
.id: stringid) ??
opts: ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        readonly maximum: string | number;
        readonly inclusive?: boolean | undefined;
        readonly exact?: boolean | undefined;
        readonly path: readonly [] | readonly [...];
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>>
opts
.
ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{ user: User | null; }, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, { ...; }>, { ...; }>, { ...; }>, { ...; }>>>.errors: ErrorConstructors<{
    NOT_FOUND: TrpcError<"NOT_FOUND", {
        id: string;
    }>;
}>
errors
.
type NOT_FOUND: (opts: ErrorConstructorOpts<{
    id: string;
}>) => TrpcError<"NOT_FOUND", {
    id: string;
}>
NOT_FOUND
({
data: {
    id: string;
}
data
: { id: stringid:
opts: ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{
    user: User | null;
}, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
    meta: {
        route?: Route | undefined;
    };
}>, {
    inputIn: {
        id: string;
    };
    inputOut: {
        id: string;
    };
    errors: InputValidationError<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["id"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        readonly maximum: string | number;
        readonly inclusive?: boolean | undefined;
        readonly exact?: boolean | undefined;
        readonly path: readonly [] | readonly [...];
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>;
    requires: never;
}>, {
    ...;
}>, {
    ...;
}>>>
opts
.
ResolverOpts<ImplementerDef<ExtendedDef<RootDef<{ user: User | null; }, {}>, RootDef<{}, OpenApiMeta>>, ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, { ...; }>, { ...; }>, { ...; }>, { ...; }>>>.input: {
    id: string;
}
input
.id: stringid } }),
); } } @function Module(metadata: ModuleMetadata): ClassDecoratorModule({ ModuleMetadata.imports?: (Type<any> | ForwardReference<any> | DynamicModule | Promise<DynamicModule>)[] | undefinedimports: [ class experimental_TrpcModuleexperimental_TrpcModule.experimental_TrpcModule.forRoot(options?: TrpcModuleOptions): DynamicModuleforRoot({ TrpcModuleOptions.createContext?: ((opts: CreateContextOpts<ExecutionContext>) => object | Promise<object>) | undefinedcreateContext: (opts: CreateContextOpts<ExecutionContext>opts) => ({ user: anyuser: opts: CreateContextOpts<ExecutionContext>opts.CreateContextOpts<ExecutionContext>.platform: ExecutionContextplatform.ArgumentsHost.switchToHttp(): HttpArgumentsHostswitchToHttp().HttpArgumentsHost.getRequest<any>(): anygetRequest().user, }), TrpcModuleOptions.layer?: Layer.Layer<any, any, never> | undefinedlayer: class DbDb.Db.layer: Layer.Layer<Db, never, never>layer, }), ], ModuleMetadata.controllers?: Type<any>[] | undefinedcontrollers: [class PostControllerPostController], ModuleMetadata.providers?: Provider[] | undefinedproviders: [class PostsPosts], }) class class AppModuleAppModule {}
  • A leaf needs a route path, .meta({ route: { path } }). Pass a contract router instead and return impl.router({ … }) (or a plain object of procedures) to get default paths from the keys below it (/post/byId).
  • experimental_TrpcModule.forRoot() takes the OpenAPI handler's options; createContext's platform is Nest's ExecutionContext. forRootAsync({ inject, useFactory }) builds them from providers. One handler serves every route, so the layer is built once and released on shutdown (app.enableShutdownHooks() or app.close()).
  • Guards, pipes and filters run as usual; put @experimental_Implement above the method's other decorators so they apply to every route it adds. The procedure's own middleware and validation run after them, and errors use the REST shape above.
  • Bodies Nest's JSON and urlencoded parsers read are used as parsed; others are read from the request.
  • Express only. Nest needs experimentalDecorators.

Routes and errors follow the OpenAPI handler.