ExperimentalContracts and OpenAPI might change or go away in any release. Stability
NestJS
Serve a contract from Nest controllers with @trpc12/experimental-nest.
On this page
@trpc12/experimental-nest serves a contract from Nest controllers. @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 whole package is experimental, as its name says, so its exports have no prefix: it might change or go away in any release.
pnpm add @trpc12/trpc effect @trpc12/experimental-nest @nestjs/common @nestjs/core @effect/platform-node rxjsnpm install @trpc12/trpc effect @trpc12/experimental-nest @nestjs/common @nestjs/core @effect/platform-node rxjsyarn add @trpc12/trpc effect @trpc12/experimental-nest @nestjs/common @nestjs/core @effect/platform-node rxjsbun add @trpc12/trpc effect @trpc12/experimental-nest @nestjs/common @nestjs/core @effect/platform-node rxjsimport { function Implement(contract: object): (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => voidImplement, class TrpcModuleTrpcModule } from '@trpc12/experimental-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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
...;
};
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
...;
}>>;
};
}>(contract: {
post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
meta: {
route?: Route | undefined;
};
}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
...;
};
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 Implement(contract: object): (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => voidImplement(const appContract: {
post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
meta: {
route?: Route | undefined;
};
}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
...;
}>>;
};
}
appContract.post: {
byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
meta: {
route?: Route | undefined;
};
}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
...;
}>>;
}
post.byId: ContractProcedure<"query", ContractWith<ContractWith<ContractWith<ContractWith<ContractRootDef<{}>, {
meta: {
route?: Route | undefined;
};
}>, {
inputIn: {
id: string;
};
inputOut: {
id: string;
};
errors: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
declared: {
...;
};
errors: SchemaValidationError<...> | TrpcError<...>;
}>>
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
...;
};
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
declared: {
...;
};
errors: SchemaValidationError<...> | TrpcError<...>;
}>>, 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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
...;
};
requires: never;
}>, {
...;
}>>>) => (TrpcError<...> | Post) & (AnyTrpcError | {
...;
})) => 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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
declared: {
...;
};
errors: SchemaValidationError<...> | TrpcError<...>;
}>>>
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
declared: {
...;
};
errors: SchemaValidationError<...> | TrpcError<...>;
}>>>
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
declared: {
...;
};
errors: SchemaValidationError<...> | TrpcError<...>;
}>>>
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: SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
requires: never;
}>, {
outputIn: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
outputOut: {
id: string;
title: string;
author: string | null;
createdAt: Date;
};
requires: never;
}>, {
declared: {
...;
};
errors: SchemaValidationError<...> | TrpcError<...>;
}>>>
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 TrpcModuleTrpcModule.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 returnimpl.router({ … })(or a plain object of procedures) to get default paths from the keys below it (/post/byId). TrpcModule.forRoot()takes the OpenAPI handler's options;createContext'splatformis Nest'sExecutionContext.forRootAsync({ inject, useFactory })builds them from providers. One handler serves every route, so thelayeris built once and released on shutdown (app.enableShutdownHooks()orapp.close()).- Guards, pipes and filters run as usual; put
@Implementabove 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.
The examples/experimental/nest app implements a contract in a controller, with a guard, an injected service and an Effect layer, and calls it with experimental_openApiLink.
Credits
@trpc12/experimental-nest follows oRPC's NestJS integration.