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 returnimpl.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'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
@experimental_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.