@rest-rpc/nest 0.1.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rest-rpc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,26 @@
1
+ import type { Contract, HttpRouteDeclaration } from "@rest-rpc/core/contract";
2
+ import "reflect-metadata";
3
+ export declare const REST_RPC_ROUTE_METADATA: unique symbol;
4
+ export type RouteMetadata = {
5
+ route: HttpRouteDeclaration;
6
+ };
7
+ /**
8
+ * Binds a Nest controller method to a rest-rpc HTTP contract route.
9
+ *
10
+ * @remarks Use this decorator when a controller method returns the
11
+ * implementation for a single route. The method can still use Nest decorators
12
+ * and dependency injection before handing off to `route()`.
13
+ *
14
+ * @see {@link https://rest-rpc.dev/docs/server/nest#single-routes}
15
+ */
16
+ export declare function Route(route: HttpRouteDeclaration): MethodDecorator;
17
+ /**
18
+ * Binds one Nest controller method to every HTTP route in a rest-rpc contract router.
19
+ *
20
+ * @remarks Use this decorator when one controller method returns a
21
+ * contract-shaped implementation tree from `router()`. The decorator registers
22
+ * each HTTP route with Nest while the returned tree provides the handlers.
23
+ *
24
+ * @see {@link https://rest-rpc.dev/docs/server/nest#usage}
25
+ */
26
+ export declare function Router(contract: Contract): MethodDecorator;
@@ -0,0 +1,84 @@
1
+ import { applyDecorators, RequestMethod } from "@nestjs/common";
2
+ import { METHOD_METADATA, PATH_METADATA } from "@nestjs/common/constants.js";
3
+ import { contractRouteEntries, toColonPath } from "@rest-rpc/core/contract";
4
+ import "reflect-metadata";
5
+ export const REST_RPC_ROUTE_METADATA = Symbol.for("rest-rpc:nest-route");
6
+ const methodMap = {
7
+ DELETE: RequestMethod.DELETE,
8
+ GET: RequestMethod.GET,
9
+ PATCH: RequestMethod.PATCH,
10
+ POST: RequestMethod.POST,
11
+ PUT: RequestMethod.PUT,
12
+ };
13
+ const createNestRouteDecorator = (route) => (_target, _propertyKey, descriptor) => {
14
+ if (!descriptor?.value)
15
+ return;
16
+ Reflect.defineMetadata(PATH_METADATA, toColonPath(route.path), descriptor.value);
17
+ Reflect.defineMetadata(METHOD_METADATA, methodMap[route.method], descriptor.value);
18
+ Reflect.defineMetadata(REST_RPC_ROUTE_METADATA, { route }, descriptor.value);
19
+ };
20
+ const isHttpRouteDeclaration = (route) => "responses" in route;
21
+ const getImplementationAtPath = (tree, path) => path.reduce((value, key) => value && typeof value === "object"
22
+ ? value[key]
23
+ : undefined, tree);
24
+ const copyMetadata = (from, to) => {
25
+ for (const key of Reflect.getMetadataKeys(from)) {
26
+ Reflect.defineMetadata(key, Reflect.getMetadata(key, from), to);
27
+ }
28
+ };
29
+ const copyPropertyMetadata = (target, from, to) => {
30
+ for (const key of Reflect.getMetadataKeys(target.constructor, from)) {
31
+ Reflect.defineMetadata(key, Reflect.getMetadata(key, target.constructor, from), target.constructor, to);
32
+ }
33
+ };
34
+ let routerRouteMethodId = 0;
35
+ const createRouterRouteMethod = (target, propertyKey, descriptor, route, path) => {
36
+ const original = descriptor.value;
37
+ if (typeof original !== "function")
38
+ return;
39
+ const routeMethodName = `__restRpcRouter_${String(propertyKey)}_${routerRouteMethodId++}`;
40
+ const routeMethod = async function (...args) {
41
+ const tree = await original.apply(this, args);
42
+ return getImplementationAtPath(tree, path);
43
+ };
44
+ copyMetadata(original, routeMethod);
45
+ copyPropertyMetadata(target, propertyKey, routeMethodName);
46
+ Object.defineProperty(target, routeMethodName, {
47
+ configurable: true,
48
+ value: routeMethod,
49
+ });
50
+ createNestRouteDecorator(route)(target, routeMethodName, {
51
+ ...descriptor,
52
+ value: routeMethod,
53
+ });
54
+ };
55
+ /**
56
+ * Binds a Nest controller method to a rest-rpc HTTP contract route.
57
+ *
58
+ * @remarks Use this decorator when a controller method returns the
59
+ * implementation for a single route. The method can still use Nest decorators
60
+ * and dependency injection before handing off to `route()`.
61
+ *
62
+ * @see {@link https://rest-rpc.dev/docs/server/nest#single-routes}
63
+ */
64
+ export function Route(route) {
65
+ return applyDecorators(createNestRouteDecorator(route));
66
+ }
67
+ /**
68
+ * Binds one Nest controller method to every HTTP route in a rest-rpc contract router.
69
+ *
70
+ * @remarks Use this decorator when one controller method returns a
71
+ * contract-shaped implementation tree from `router()`. The decorator registers
72
+ * each HTTP route with Nest while the returned tree provides the handlers.
73
+ *
74
+ * @see {@link https://rest-rpc.dev/docs/server/nest#usage}
75
+ */
76
+ export function Router(contract) {
77
+ return (target, propertyKey, descriptor) => {
78
+ for (const { route, path } of contractRouteEntries(contract)) {
79
+ if (!isHttpRouteDeclaration(route))
80
+ continue;
81
+ createRouterRouteMethod(target, propertyKey, descriptor, route, path);
82
+ }
83
+ };
84
+ }
@@ -0,0 +1,2 @@
1
+ import type { NestHttpPlatform } from "./httpPlatform.ts";
2
+ export declare const createExpressHttpPlatform: (req: unknown, res: unknown) => NestHttpPlatform | undefined;
@@ -0,0 +1,120 @@
1
+ import { formatSseEvent } from "@rest-rpc/server";
2
+ import { hasFunction } from "./httpPlatform.js";
3
+ const isExpressLikeResponse = (value) => hasFunction(value, "status") &&
4
+ hasFunction(value, "send") &&
5
+ hasFunction(value, "setHeader");
6
+ const createExpressRequestSignal = (req, res) => {
7
+ const controller = new AbortController();
8
+ const abort = () => controller.abort();
9
+ req.once?.("aborted", abort);
10
+ res.once?.("close", () => {
11
+ if (!res.writableFinished)
12
+ abort();
13
+ });
14
+ return controller.signal;
15
+ };
16
+ const writeExpressStreamResponse = async (res, { body, status, contentType, mode, signal }) => {
17
+ res.status(status);
18
+ res.setHeader("content-type", contentType);
19
+ const iterator = body[Symbol.asyncIterator]();
20
+ let closed = false;
21
+ let finished = false;
22
+ const closeIterator = async () => {
23
+ try {
24
+ await iterator.return?.();
25
+ }
26
+ catch { }
27
+ };
28
+ const onClose = () => {
29
+ if (finished)
30
+ return;
31
+ closed = true;
32
+ void closeIterator();
33
+ };
34
+ res.on?.("close", onClose);
35
+ signal.addEventListener("abort", onClose, { once: true });
36
+ const waitForDrain = async () => {
37
+ if (!res.once)
38
+ return;
39
+ await new Promise((resolve, reject) => {
40
+ const cleanup = () => {
41
+ res.off?.("drain", onDrain);
42
+ res.off?.("close", onClose);
43
+ res.off?.("error", onError);
44
+ };
45
+ const onDrain = () => {
46
+ cleanup();
47
+ resolve();
48
+ };
49
+ const onClose = () => {
50
+ cleanup();
51
+ resolve();
52
+ };
53
+ const onError = (error) => {
54
+ cleanup();
55
+ reject(error);
56
+ };
57
+ res.once?.("drain", onDrain);
58
+ res.once?.("close", onClose);
59
+ res.once?.("error", onError);
60
+ });
61
+ };
62
+ try {
63
+ while (!closed) {
64
+ const { done, value: chunk } = await iterator.next();
65
+ if (done || closed)
66
+ break;
67
+ if (mode === "ndjson") {
68
+ const canContinue = res.write?.(`${JSON.stringify(chunk)}\n`);
69
+ if (canContinue === false && !closed)
70
+ await waitForDrain();
71
+ continue;
72
+ }
73
+ if (mode === "sse") {
74
+ const canContinue = res.write?.(formatSseEvent(chunk));
75
+ if (canContinue === false && !closed)
76
+ await waitForDrain();
77
+ continue;
78
+ }
79
+ const canContinue = res.write?.(chunk);
80
+ if (canContinue === false && !closed)
81
+ await waitForDrain();
82
+ }
83
+ finished = true;
84
+ if (!closed)
85
+ res.end();
86
+ }
87
+ catch (error) {
88
+ res.destroy?.(error instanceof Error ? error : undefined);
89
+ }
90
+ finally {
91
+ finished = true;
92
+ signal.removeEventListener("abort", onClose);
93
+ res.off?.("close", onClose);
94
+ }
95
+ };
96
+ const createExpressReply = (res) => ({
97
+ setHeader: (name, value) => res.setHeader(name, value),
98
+ sendEmpty: (status) => {
99
+ res.status(status);
100
+ return undefined;
101
+ },
102
+ sendJson: (status, body) => {
103
+ res.status(status);
104
+ return body;
105
+ },
106
+ sendCustom: (status, body) => {
107
+ res.status(status);
108
+ res.send(body);
109
+ return undefined;
110
+ },
111
+ sendStream: (input) => writeExpressStreamResponse(res, input),
112
+ });
113
+ export const createExpressHttpPlatform = (req, res) => {
114
+ if (!isExpressLikeResponse(res))
115
+ return undefined;
116
+ return {
117
+ signal: createExpressRequestSignal(req, res),
118
+ reply: createExpressReply(res),
119
+ };
120
+ };
@@ -0,0 +1,2 @@
1
+ import type { NestHttpPlatform } from "./httpPlatform.ts";
2
+ export declare const createFastifyHttpPlatform: (req: unknown, res: unknown) => NestHttpPlatform | undefined;
@@ -0,0 +1,73 @@
1
+ import { Readable } from "node:stream";
2
+ import { formatSseEvent } from "@rest-rpc/server";
3
+ import { hasFunction } from "./httpPlatform.js";
4
+ const isFastifyLikeResponse = (value) => hasFunction(value, "send") &&
5
+ (hasFunction(value, "header") || hasFunction(value, "code"));
6
+ const setStatus = (res, statusCode) => {
7
+ if (res.code) {
8
+ res.code(statusCode);
9
+ return;
10
+ }
11
+ res.status?.(statusCode);
12
+ };
13
+ const setHeader = (res, name, value) => {
14
+ if (res.header) {
15
+ res.header(name, value);
16
+ return;
17
+ }
18
+ res.raw?.setHeader?.(name, value);
19
+ };
20
+ const createFastifyRequestSignal = (req, res) => {
21
+ const controller = new AbortController();
22
+ const abort = () => controller.abort();
23
+ req.raw?.once?.("aborted", abort);
24
+ res.raw?.once?.("close", () => {
25
+ if (!res.raw?.writableFinished)
26
+ abort();
27
+ });
28
+ return controller.signal;
29
+ };
30
+ const toNodeStream = ({ body, mode }) => Readable.from((async function* () {
31
+ for await (const chunk of body) {
32
+ if (mode === "ndjson") {
33
+ yield `${JSON.stringify(chunk)}\n`;
34
+ continue;
35
+ }
36
+ if (mode === "sse") {
37
+ yield formatSseEvent(chunk);
38
+ continue;
39
+ }
40
+ yield chunk;
41
+ }
42
+ })());
43
+ const createFastifyReply = (res) => ({
44
+ setHeader: (name, value) => setHeader(res, name, value),
45
+ sendEmpty: (status) => {
46
+ setStatus(res, status);
47
+ res.send();
48
+ return undefined;
49
+ },
50
+ sendJson: (status, body) => {
51
+ setStatus(res, status);
52
+ res.send(body);
53
+ return undefined;
54
+ },
55
+ sendCustom: (status, body) => {
56
+ setStatus(res, status);
57
+ res.send(body);
58
+ return undefined;
59
+ },
60
+ sendStream: (input) => {
61
+ setStatus(res, input.status);
62
+ setHeader(res, "content-type", input.contentType);
63
+ return toNodeStream(input);
64
+ },
65
+ });
66
+ export const createFastifyHttpPlatform = (req, res) => {
67
+ if (!isFastifyLikeResponse(res))
68
+ return undefined;
69
+ return {
70
+ signal: createFastifyRequestSignal(req, res),
71
+ reply: createFastifyReply(res),
72
+ };
73
+ };
@@ -0,0 +1,27 @@
1
+ import type { HttpRouteResultStreamMode } from "@rest-rpc/server";
2
+ export type NestHttpRequest = {
3
+ body?: unknown;
4
+ query?: unknown;
5
+ params?: unknown;
6
+ headers?: unknown;
7
+ };
8
+ export type NestStreamResponseInput = {
9
+ body: AsyncIterable<unknown>;
10
+ status: number;
11
+ contentType: string;
12
+ mode: HttpRouteResultStreamMode;
13
+ signal: AbortSignal;
14
+ };
15
+ export type NestHttpReply = {
16
+ setHeader(name: string, value: unknown): void;
17
+ sendEmpty(status: number): unknown;
18
+ sendJson(status: number, body: unknown): unknown;
19
+ sendCustom(status: number, body: unknown): unknown;
20
+ sendStream(input: NestStreamResponseInput): unknown;
21
+ };
22
+ export type NestHttpPlatform = {
23
+ signal: AbortSignal;
24
+ reply: NestHttpReply;
25
+ };
26
+ export declare const hasFunction: <TName extends string>(value: unknown, name: TName) => value is Record<TName, (...args: never[]) => unknown>;
27
+ export declare const createNestHttpPlatform: (req: unknown, res: unknown) => NestHttpPlatform;
@@ -0,0 +1,12 @@
1
+ import { createExpressHttpPlatform } from "./expressPlatform.js";
2
+ import { createFastifyHttpPlatform } from "./fastifyPlatform.js";
3
+ export const hasFunction = (value, name) => typeof value === "object" &&
4
+ value !== null &&
5
+ typeof value[name] === "function";
6
+ export const createNestHttpPlatform = (req, res) => {
7
+ const platform = createExpressHttpPlatform(req, res) ?? createFastifyHttpPlatform(req, res);
8
+ if (!platform) {
9
+ throw new Error("Unsupported Nest HTTP platform. @rest-rpc/nest supports the Nest Express and Fastify adapters.");
10
+ }
11
+ return platform;
12
+ };
@@ -0,0 +1,93 @@
1
+ import type { HttpRouteDeclaration } from "@rest-rpc/core/contract";
2
+ import { type Contract, type ImplementationTreeFor, type RouteImplementation, type RouteHandler as ServerRouteHandler, type RouteRequest as ServerRouteRequest } from "@rest-rpc/server";
3
+ import type { DefaultNestContext, NestHandlerContext } from "./module.ts";
4
+ export type { ClearCookieOptions, RouteErrors, RouteResponse, RouteResponseShorthand, SetCookieOptions, SseEvent, } from "@rest-rpc/server";
5
+ export { clearCookie, RouteResponseError, setCookie, sseEvent, } from "@rest-rpc/server";
6
+ export { Route, Router } from "./decorators.ts";
7
+ export type { DefaultNestContext, NestHandlerContext, RestRpcModuleOptions, } from "./module.ts";
8
+ export { RestRpcModule } from "./module.ts";
9
+ /**
10
+ * A contract tree containing only HTTP routes for the Nest adapter.
11
+ *
12
+ * @remarks The Nest adapter currently registers HTTP routes through Nest
13
+ * controllers. Use this helper to constrain router contracts passed to
14
+ * `router()` and `RouteHandlers`.
15
+ *
16
+ * @see {@link https://rest-rpc.dev/docs/server/nest}
17
+ */
18
+ export type NestContract = Contract<HttpRouteDeclaration>;
19
+ type AdditionalNestContext<TContext extends Record<string, unknown>> = DefaultNestContext & TContext;
20
+ /**
21
+ * Infers the Nest route handler request type for a given route declaration.
22
+ *
23
+ * @remarks The inferred request includes the route input fields and a
24
+ * Nest-specific `context` property. Pass the second generic argument for
25
+ * controller-local context values added by `router(..., { context })`.
26
+ *
27
+ * @see {@link https://rest-rpc.dev/docs/type-helpers#server}
28
+ * @see {@link https://rest-rpc.dev/docs/server/nest#controller-local-context}
29
+ */
30
+ export type RouteRequest<E extends HttpRouteDeclaration, TAdditionalContext extends Record<string, unknown> = Record<never, never>> = ServerRouteRequest<E, NestHandlerContext<AdditionalNestContext<TAdditionalContext>>>;
31
+ /**
32
+ * Infers the Nest route handler type for a given route declaration.
33
+ *
34
+ * @remarks Use this type when annotating reusable handler functions for a
35
+ * single route. The handler receives the same context shape used by
36
+ * `RouteRequest`.
37
+ *
38
+ * @see {@link https://rest-rpc.dev/docs/type-helpers#server}
39
+ * @see {@link https://rest-rpc.dev/docs/server/nest#framework-context}
40
+ */
41
+ export type RouteHandler<E extends HttpRouteDeclaration, TAdditionalContext extends Record<string, unknown> = Record<never, never>> = ServerRouteHandler<E, NestHandlerContext<AdditionalNestContext<TAdditionalContext>>>;
42
+ type BivariantRouteHandler<E extends HttpRouteDeclaration> = {
43
+ handler(...args: Parameters<RouteHandler<E>>): ReturnType<RouteHandler<E>>;
44
+ }["handler"];
45
+ /**
46
+ * Handler tree accepted by `router()` when building a Nest implementation tree.
47
+ *
48
+ * @remarks Use this type with `implements` to check injectable Nest provider
49
+ * classes against a contract tree.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * @Injectable()
54
+ * class TodoHandlers implements RouteHandlers<typeof api.todos> {
55
+ * get(request: RouteRequest<typeof api.todos.get>) {
56
+ * return { id: request.id };
57
+ * }
58
+ * }
59
+ * ```
60
+ *
61
+ * @see {@link https://rest-rpc.dev/docs/server/nest#usage}
62
+ */
63
+ export type RouteHandlers<TContract extends NestContract> = TContract extends HttpRouteDeclaration ? BivariantRouteHandler<TContract> | RouteImplementation<TContract> : {
64
+ [K in keyof TContract]: TContract[K] extends NestContract ? RouteHandlers<TContract[K]> : never;
65
+ };
66
+ /**
67
+ * Builds a Nest route implementation for a single contract route.
68
+ *
69
+ * @remarks Return this from a controller method decorated with `@Route()`. The
70
+ * optional `context` value is merged into the runtime handler context for that
71
+ * route.
72
+ *
73
+ * @see {@link https://rest-rpc.dev/docs/server/nest#single-routes}
74
+ * @see {@link https://rest-rpc.dev/docs/server/nest#controller-local-context}
75
+ */
76
+ export declare function route<const TRoute extends HttpRouteDeclaration, TContext extends Record<string, unknown> = Record<never, never>>(contract: TRoute, handler: RouteHandler<TRoute, TContext>, options?: {
77
+ context?: Record<string, unknown>;
78
+ }): RouteImplementation<TRoute> & {
79
+ context?: Record<string, unknown>;
80
+ };
81
+ /**
82
+ * Builds a Nest router implementation for a contract.
83
+ *
84
+ * @remarks Return this from a controller method decorated with `@Router()`. The
85
+ * optional `context` value is merged into the runtime handler context for every
86
+ * route in the returned implementation tree.
87
+ *
88
+ * @see {@link https://rest-rpc.dev/docs/server/nest#usage}
89
+ * @see {@link https://rest-rpc.dev/docs/server/nest#controller-local-context}
90
+ */
91
+ export declare function router<const TContract extends NestContract>(contract: TContract, handlers: RouteHandlers<TContract>, options?: {
92
+ context?: Record<string, unknown>;
93
+ }): ImplementationTreeFor<TContract, HttpRouteDeclaration>;
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import { route as serverRoute, router as serverRouter, } from "@rest-rpc/server";
2
+ export { clearCookie, RouteResponseError, setCookie, sseEvent, } from "@rest-rpc/server";
3
+ export { Route, Router } from "./decorators.js";
4
+ export { RestRpcModule } from "./module.js";
5
+ const isNestRouteImplementation = (value) => typeof value === "object" &&
6
+ value !== null &&
7
+ "route" in value &&
8
+ "handler" in value;
9
+ const attachNestRouteContext = (implementation, context) => {
10
+ if (context === undefined)
11
+ return implementation;
12
+ if (isNestRouteImplementation(implementation)) {
13
+ return {
14
+ ...implementation,
15
+ context,
16
+ };
17
+ }
18
+ return Object.fromEntries(Object.entries(implementation).map(([key, child]) => [
19
+ key,
20
+ attachNestRouteContext(child, context),
21
+ ]));
22
+ };
23
+ /**
24
+ * Builds a Nest route implementation for a single contract route.
25
+ *
26
+ * @remarks Return this from a controller method decorated with `@Route()`. The
27
+ * optional `context` value is merged into the runtime handler context for that
28
+ * route.
29
+ *
30
+ * @see {@link https://rest-rpc.dev/docs/server/nest#single-routes}
31
+ * @see {@link https://rest-rpc.dev/docs/server/nest#controller-local-context}
32
+ */
33
+ export function route(contract, handler, options = {}) {
34
+ return attachNestRouteContext(serverRoute(contract, handler), options.context);
35
+ }
36
+ /**
37
+ * Builds a Nest router implementation for a contract.
38
+ *
39
+ * @remarks Return this from a controller method decorated with `@Router()`. The
40
+ * optional `context` value is merged into the runtime handler context for every
41
+ * route in the returned implementation tree.
42
+ *
43
+ * @see {@link https://rest-rpc.dev/docs/server/nest#usage}
44
+ * @see {@link https://rest-rpc.dev/docs/server/nest#controller-local-context}
45
+ */
46
+ export function router(contract, handlers, options = {}) {
47
+ return attachNestRouteContext(serverRouter(contract, handlers), options.context);
48
+ }
@@ -0,0 +1,60 @@
1
+ import type { DynamicModule, ExecutionContext } from "@nestjs/common";
2
+ import type { ServerErrorHandlers } from "@rest-rpc/server";
3
+ /**
4
+ * Default application context passed to Nest route handlers.
5
+ *
6
+ * @remarks Augment this interface to set the route handler context across a
7
+ * project. The augmented shape is used by `RouteRequest`, `RouteHandler`,
8
+ * `RouteHandlers`, `route()`, and `router()`.
9
+ *
10
+ * @see {@link https://rest-rpc.dev/docs/server/nest#global-context}
11
+ */
12
+ export interface DefaultNestContext {
13
+ }
14
+ interface ContextShape {
15
+ [key: string]: any;
16
+ }
17
+ type Merge<T> = {
18
+ [K in keyof T]: T[K];
19
+ };
20
+ /**
21
+ * The context object passed to Nest adapter route handlers.
22
+ *
23
+ * @remarks This combines the application context returned by `createContext`
24
+ * with the adapter-supplied `AbortSignal`.
25
+ *
26
+ * @see {@link https://rest-rpc.dev/docs/server/nest#framework-context}
27
+ */
28
+ export type NestHandlerContext<TContext extends ContextShape = DefaultNestContext> = Merge<TContext & {
29
+ signal: AbortSignal;
30
+ }>;
31
+ /**
32
+ * Options for configuring the rest-rpc Nest adapter.
33
+ *
34
+ * @remarks Use `createContext` for request-scoped values shared by all
35
+ * rest-rpc Nest handlers, and `errorHandlers` to customize validation and
36
+ * unhandled error responses.
37
+ *
38
+ * @see {@link https://rest-rpc.dev/docs/server/nest#options}
39
+ */
40
+ export type RestRpcModuleOptions<TContext extends ContextShape = DefaultNestContext> = {
41
+ createContext?: (context: ExecutionContext) => TContext | Promise<TContext>;
42
+ errorHandlers?: ServerErrorHandlers<NestHandlerContext<TContext>>;
43
+ };
44
+ /**
45
+ * Configures rest-rpc route handling for Nest controllers.
46
+ *
47
+ * @remarks Import `RestRpcModule.forRoot()` once in a Nest module to register
48
+ * the global interceptor used by `@Route()` and `@Router()`.
49
+ *
50
+ * @see {@link https://rest-rpc.dev/docs/server/nest#usage}
51
+ */
52
+ export declare class RestRpcModule {
53
+ /**
54
+ * Registers the global interceptor used by rest-rpc Nest route decorators.
55
+ *
56
+ * @see {@link https://rest-rpc.dev/docs/server/nest#options}
57
+ */
58
+ static forRoot<TContext extends ContextShape = DefaultNestContext>(options?: RestRpcModuleOptions<TContext>): DynamicModule;
59
+ }
60
+ export {};
package/dist/module.js ADDED
@@ -0,0 +1,51 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var RestRpcModule_1;
8
+ import { Module } from "@nestjs/common";
9
+ import { APP_INTERCEPTOR } from "@nestjs/core";
10
+ import { RestRpcRouteInterceptor } from "./routeInterceptor.js";
11
+ /**
12
+ * Configures rest-rpc route handling for Nest controllers.
13
+ *
14
+ * @remarks Import `RestRpcModule.forRoot()` once in a Nest module to register
15
+ * the global interceptor used by `@Route()` and `@Router()`.
16
+ *
17
+ * @see {@link https://rest-rpc.dev/docs/server/nest#usage}
18
+ */
19
+ let RestRpcModule = RestRpcModule_1 = class RestRpcModule {
20
+ /**
21
+ * Registers the global interceptor used by rest-rpc Nest route decorators.
22
+ *
23
+ * @see {@link https://rest-rpc.dev/docs/server/nest#options}
24
+ */
25
+ static forRoot(options = {}) {
26
+ const restRpcModuleOptions = Symbol.for("rest-rpc:nest-options");
27
+ return {
28
+ module: RestRpcModule_1,
29
+ providers: [
30
+ {
31
+ provide: restRpcModuleOptions,
32
+ useValue: options,
33
+ },
34
+ {
35
+ provide: RestRpcRouteInterceptor,
36
+ inject: [restRpcModuleOptions],
37
+ useFactory: (moduleOptions) => new RestRpcRouteInterceptor(moduleOptions),
38
+ },
39
+ {
40
+ provide: APP_INTERCEPTOR,
41
+ useExisting: RestRpcRouteInterceptor,
42
+ },
43
+ ],
44
+ exports: [RestRpcRouteInterceptor],
45
+ };
46
+ }
47
+ };
48
+ RestRpcModule = RestRpcModule_1 = __decorate([
49
+ Module({})
50
+ ], RestRpcModule);
51
+ export { RestRpcModule };
@@ -0,0 +1,9 @@
1
+ import { type CallHandler, type ExecutionContext, type NestInterceptor } from "@nestjs/common";
2
+ import type { Observable } from "rxjs";
3
+ import type { RestRpcModuleOptions } from "./module.ts";
4
+ export declare class RestRpcRouteInterceptor implements NestInterceptor {
5
+ constructor(options?: RestRpcModuleOptions<Record<string, unknown>>);
6
+ private readonly options?;
7
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
8
+ private handle;
9
+ }
@@ -0,0 +1,71 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Injectable, } from "@nestjs/common";
8
+ import { handleHttpRoute, handleHttpRouteResult, isHttpRouteImplementation, } from "@rest-rpc/server";
9
+ import { from, lastValueFrom } from "rxjs";
10
+ import { REST_RPC_ROUTE_METADATA } from "./decorators.js";
11
+ import { createNestHttpPlatform, } from "./httpPlatform.js";
12
+ const assertRouteImplementation = (value, route) => {
13
+ if (!isHttpRouteImplementation(value)) {
14
+ throw new Error(`Controller method for "${route.method} ${route.path}" must return a rest-rpc route implementation.`);
15
+ }
16
+ const implementation = value;
17
+ if (implementation.route.method !== route.method ||
18
+ implementation.route.path !== route.path) {
19
+ throw new Error(`Controller method for "${route.method} ${route.path}" returned an implementation for "${implementation.route.method} ${implementation.route.path}".`);
20
+ }
21
+ return implementation;
22
+ };
23
+ let RestRpcRouteInterceptor = class RestRpcRouteInterceptor {
24
+ constructor(options) {
25
+ this.options = options;
26
+ }
27
+ options;
28
+ intercept(context, next) {
29
+ const metadata = Reflect.getMetadata(REST_RPC_ROUTE_METADATA, context.getHandler());
30
+ if (!metadata)
31
+ return next.handle();
32
+ return from(this.handle(context, next, metadata));
33
+ }
34
+ async handle(context, next, metadata) {
35
+ const http = context.switchToHttp();
36
+ const req = http.getRequest();
37
+ const res = http.getResponse();
38
+ const { signal, reply } = createNestHttpPlatform(req, res);
39
+ const userContext = await this.options?.createContext?.(context);
40
+ const implementation = assertRouteImplementation(await lastValueFrom(next.handle()), metadata.route);
41
+ const routeContext = {
42
+ ...userContext,
43
+ ...implementation.context,
44
+ signal,
45
+ };
46
+ const result = await handleHttpRoute(metadata.route, implementation.handler, {
47
+ request: {
48
+ body: req.body,
49
+ query: req.query,
50
+ pathParams: req.params,
51
+ headers: req.headers,
52
+ },
53
+ context: routeContext,
54
+ errorHandlers: this.options?.errorHandlers,
55
+ });
56
+ return handleHttpRouteResult(result, {
57
+ setHeader: (name, value) => {
58
+ if (value !== undefined)
59
+ reply.setHeader(name, value);
60
+ },
61
+ sendEmpty: (status) => reply.sendEmpty(status),
62
+ sendJson: (status, body) => reply.sendJson(status, body),
63
+ sendCustom: (status, body) => reply.sendCustom(status, body),
64
+ sendStream: ({ body, status, contentType, mode }) => reply.sendStream({ body, status, contentType, mode, signal }),
65
+ });
66
+ }
67
+ };
68
+ RestRpcRouteInterceptor = __decorate([
69
+ Injectable()
70
+ ], RestRpcRouteInterceptor);
71
+ export { RestRpcRouteInterceptor };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@rest-rpc/nest",
3
+ "version": "0.1.0-beta.14",
4
+ "description": "NestJS adapter for serving rest-rpc contracts with typed HTTP routes.",
5
+ "keywords": [
6
+ "api-contract",
7
+ "contract-first",
8
+ "http",
9
+ "nest",
10
+ "nestjs",
11
+ "rest",
12
+ "rest-rpc",
13
+ "rpc",
14
+ "server",
15
+ "type-safe-api",
16
+ "typescript"
17
+ ],
18
+ "homepage": "https://github.com/rest-rpc/rest-rpc#readme",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/rest-rpc/rest-rpc.git",
23
+ "directory": "packages/nest"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "dependencies": {
38
+ "@rest-rpc/core": "0.1.0-beta.14",
39
+ "@rest-rpc/server": "0.1.0-beta.14"
40
+ },
41
+ "devDependencies": {
42
+ "@nestjs/common": "^11.0.0",
43
+ "@nestjs/core": "^11.0.0",
44
+ "@nestjs/platform-express": "^11.0.0",
45
+ "@nestjs/platform-fastify": "^11.2.3",
46
+ "@types/express": "^5.0.1",
47
+ "@types/node": "^26.3.0",
48
+ "reflect-metadata": "^0.2.2",
49
+ "rxjs": "^7.8.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@nestjs/common": "^11.0.0",
53
+ "@nestjs/core": "^11.0.0",
54
+ "@nestjs/platform-express": "^11.0.0",
55
+ "@nestjs/platform-fastify": "^11.0.0",
56
+ "reflect-metadata": "^0.2.0",
57
+ "rxjs": "^7.8.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@nestjs/platform-express": {
61
+ "optional": true
62
+ },
63
+ "@nestjs/platform-fastify": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "tsd": {
68
+ "directory": "test-d"
69
+ },
70
+ "scripts": {
71
+ "build": "tsc -p tsconfig.build.json",
72
+ "typecheck": "pnpm run build && tsc -p tsconfig.json && tsd"
73
+ }
74
+ }