@rest-rpc/fetch 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 +21 -0
- package/README.md +82 -0
- package/dist/http.d.ts +20 -0
- package/dist/http.js +63 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +57 -0
- package/dist/match.d.ts +7 -0
- package/dist/match.js +30 -0
- package/dist/routeBuilder.d.ts +105 -0
- package/dist/routeBuilder.js +29 -0
- package/package.json +50 -0
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# rest-rpc
|
|
2
|
+
|
|
3
|
+
REST-shaped APIs with function-shaped TypeScript.
|
|
4
|
+
|
|
5
|
+
Define your HTTP API once as a shared TypeScript contract, then derive typed
|
|
6
|
+
server handlers, fetch clients, openAPI documents, and more from it.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Shared TypeScript contracts for HTTP APIs.
|
|
11
|
+
- Typed server handlers for Express, Hono, Fastify, NestJS, Next.js, and Fetch runtimes.
|
|
12
|
+
- Typed fetch client.
|
|
13
|
+
- Typed TanStack Query helpers.
|
|
14
|
+
- Typed WebSockets, streaming, non-JSON requests/responses.
|
|
15
|
+
- OpenAPI documents generated from the TypeScript contract.
|
|
16
|
+
- Standard Schema support.
|
|
17
|
+
|
|
18
|
+
## Minimal Example
|
|
19
|
+
|
|
20
|
+
Define a contract
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { router } from "@rest-rpc/core";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
export const api = router({
|
|
27
|
+
todos: {
|
|
28
|
+
get: {
|
|
29
|
+
method: "GET",
|
|
30
|
+
path: "/todos/:id",
|
|
31
|
+
response: z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
title: z.string(),
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Implement the contract on the server
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const routes = router(api, {
|
|
44
|
+
todos: {
|
|
45
|
+
get({ id }) {
|
|
46
|
+
return getTodo(id);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
registerRoutes(app, routes);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
use the contract on the client with RPC-style function calls
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { initClient } from "@rest-rpc/core";
|
|
58
|
+
import { api } from "./contract";
|
|
59
|
+
|
|
60
|
+
const client = initClient(api, {
|
|
61
|
+
baseUrl: "https://api.example.com",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const todo = await client.todos.get.fetch({
|
|
65
|
+
id: "todo_1",
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
Full documentation is available at [rest-rpc.dev](https://rest-rpc.dev)
|
|
72
|
+
|
|
73
|
+
## Packages
|
|
74
|
+
|
|
75
|
+
- [`@rest-rpc/core`](https://npmx.dev/package/@rest-rpc/core): API contract, client and openAPI generation.
|
|
76
|
+
- [`@rest-rpc/express`](https://npmx.dev/package/@rest-rpc/express): Express server adapter.
|
|
77
|
+
- [`@rest-rpc/fastify`](https://npmx.dev/package/@rest-rpc/fastify): Fastify server adapter.
|
|
78
|
+
- [`@rest-rpc/hono`](https://npmx.dev/package/@rest-rpc/hono): Hono server adapter.
|
|
79
|
+
- [`@rest-rpc/nest`](https://npmx.dev/package/@rest-rpc/nest): NestJS server adapter.
|
|
80
|
+
- [`@rest-rpc/next`](https://npmx.dev/package/@rest-rpc/next): Next.js adapter.
|
|
81
|
+
- [`@rest-rpc/fetch`](https://npmx.dev/package/@rest-rpc/fetch): Fetch runtime adapter
|
|
82
|
+
- [`@rest-rpc/tanstack-query`](https://npmx.dev/package/@rest-rpc/tanstack-query): TanStack Query adapter.
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HttpRouteDeclaration, RequestBodySchema } from "@rest-rpc/core/contract";
|
|
2
|
+
import { type RouteImplementation, type ServerErrorHandlers } from "@rest-rpc/server";
|
|
3
|
+
/**
|
|
4
|
+
* Input passed to a custom Fetch runtime request body parser.
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#body-parsing}
|
|
7
|
+
*/
|
|
8
|
+
export type FetchRouteParseBodyInput = {
|
|
9
|
+
request: Request;
|
|
10
|
+
route: HttpRouteDeclaration;
|
|
11
|
+
body: RequestBodySchema;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Custom request body parser for the Fetch runtime.
|
|
15
|
+
*
|
|
16
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#body-parsing}
|
|
17
|
+
*/
|
|
18
|
+
export type FetchRouteParseBody = (input: FetchRouteParseBodyInput) => unknown | Promise<unknown>;
|
|
19
|
+
export declare const defaultParseBody: ({ request, body, }: FetchRouteParseBodyInput) => Promise<any> | undefined;
|
|
20
|
+
export declare const handleFetchRoute: <TContext extends Record<string, unknown>>(request: Request, context: TContext, implementation: RouteImplementation<HttpRouteDeclaration>, params: Record<string, string>, parseBody: FetchRouteParseBody, usesDefaultParseBody: boolean, errorHandlers: ServerErrorHandlers<Record<never, never>> | undefined) => Promise<Response>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isCustomBody, isFormBody, isMultipartBody, isNoBody, } from "@rest-rpc/core/contract";
|
|
2
|
+
import { createRequestParsingErrorResponse, createFetchResponse, handleHttpRoute, } from "@rest-rpc/server";
|
|
3
|
+
const isJsonContentType = (contentType) => contentType.split(";")[0]?.trim().toLowerCase() === "application/json";
|
|
4
|
+
const isFormUrlEncodedContentType = (contentType) => contentType.split(";")[0]?.trim().toLowerCase() ===
|
|
5
|
+
"application/x-www-form-urlencoded";
|
|
6
|
+
const isMultipartFormDataContentType = (contentType) => contentType.split(";")[0]?.trim().toLowerCase() === "multipart/form-data";
|
|
7
|
+
const readQuery = (url) => Object.fromEntries(url.searchParams.entries());
|
|
8
|
+
const readHeaders = (headers) => Object.fromEntries(headers.entries());
|
|
9
|
+
export const defaultParseBody = ({ request, body, }) => {
|
|
10
|
+
if (!body || isNoBody(body))
|
|
11
|
+
return undefined;
|
|
12
|
+
if (isFormBody(body)) {
|
|
13
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
14
|
+
return isFormUrlEncodedContentType(contentType)
|
|
15
|
+
? request.text().then((text) => new URLSearchParams(text))
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
if (isMultipartBody(body)) {
|
|
19
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
20
|
+
return isMultipartFormDataContentType(contentType)
|
|
21
|
+
? request.formData()
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
if (isCustomBody(body)) {
|
|
25
|
+
const contentType = request.headers.get("content-type") ??
|
|
26
|
+
(Array.isArray(body.contentType)
|
|
27
|
+
? body.contentType[0]
|
|
28
|
+
: body.contentType);
|
|
29
|
+
return contentType && isJsonContentType(contentType)
|
|
30
|
+
? request.json()
|
|
31
|
+
: request.text();
|
|
32
|
+
}
|
|
33
|
+
return request.json();
|
|
34
|
+
};
|
|
35
|
+
export const handleFetchRoute = async (request, context, implementation, params, parseBody, usesDefaultParseBody, errorHandlers) => {
|
|
36
|
+
const url = new URL(request.url);
|
|
37
|
+
let body;
|
|
38
|
+
try {
|
|
39
|
+
body = await parseBody({
|
|
40
|
+
request,
|
|
41
|
+
route: implementation.route,
|
|
42
|
+
body: implementation.route.body,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!usesDefaultParseBody)
|
|
47
|
+
throw error;
|
|
48
|
+
return Response.json(createRequestParsingErrorResponse().body, {
|
|
49
|
+
status: 400,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const result = await handleHttpRoute(implementation.route, implementation.handler, {
|
|
53
|
+
request: {
|
|
54
|
+
body,
|
|
55
|
+
query: readQuery(url),
|
|
56
|
+
pathParams: params,
|
|
57
|
+
headers: readHeaders(request.headers),
|
|
58
|
+
},
|
|
59
|
+
context,
|
|
60
|
+
errorHandlers,
|
|
61
|
+
});
|
|
62
|
+
return createFetchResponse(result);
|
|
63
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { HttpRouteDeclaration } from "@rest-rpc/core/contract";
|
|
2
|
+
import { clearCookie, RouteResponseError, type ServerErrorHandlers, setCookie, sseEvent } from "@rest-rpc/server";
|
|
3
|
+
import { type FetchRouteParseBody } from "./http.ts";
|
|
4
|
+
import { type FetchContract, type FetchImplementationTree, type FetchRouteBuilder, type FetchRouterBuilder } from "./routeBuilder.ts";
|
|
5
|
+
export type { ClearCookieOptions, RouteErrors, RouteHandler, RouteRequestData, RouteResponse, RouteResponseShorthand, SetCookieOptions, SseEvent, } from "@rest-rpc/server";
|
|
6
|
+
export type { FetchRouteParseBody, FetchRouteParseBodyInput } from "./http.ts";
|
|
7
|
+
export type { RouteHandlers, RouteRequest, FetchContract, FetchImplementationTree, FetchRouteBuilder, FetchRouteContext, FetchRouteMiddleware, FetchRouteMiddlewareInput, FetchRouteMiddlewareResult, FetchRouterBuilder, } from "./routeBuilder.ts";
|
|
8
|
+
export { clearCookie, RouteResponseError, setCookie, sseEvent };
|
|
9
|
+
/**
|
|
10
|
+
* Default runtime context passed to Fetch runtime route handlers.
|
|
11
|
+
*
|
|
12
|
+
* @remarks Augment this interface to set the runtime context for
|
|
13
|
+
* `createRouteHandler()`, `route()`, and `router()` across a project.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* declare module "@rest-rpc/fetch" {
|
|
18
|
+
* interface DefaultRuntimeContext {
|
|
19
|
+
* env: Env;
|
|
20
|
+
* ctx: ExecutionContext;
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#framework-context}
|
|
26
|
+
*/
|
|
27
|
+
export interface DefaultRuntimeContext {
|
|
28
|
+
}
|
|
29
|
+
interface ContextShape {
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Default request type passed to Fetch runtime route handlers.
|
|
34
|
+
*
|
|
35
|
+
* @remarks Augment this interface when using a `Request` subclass such as
|
|
36
|
+
* `NextRequest`.
|
|
37
|
+
*
|
|
38
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#framework-context}
|
|
39
|
+
*/
|
|
40
|
+
export interface DefaultRequest extends Request {
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Options for creating a Fetch runtime `Request` to `Response` route handler.
|
|
44
|
+
*
|
|
45
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#options}
|
|
46
|
+
*/
|
|
47
|
+
export type CreateFetchHandlerOptions = {
|
|
48
|
+
errorHandlers?: ServerErrorHandlers<Record<never, never>>;
|
|
49
|
+
parseBody?: FetchRouteParseBody;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Creates a Fetch runtime route implementation builder for a single HTTP route.
|
|
53
|
+
*
|
|
54
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
55
|
+
*/
|
|
56
|
+
export declare function route<const TRoute extends HttpRouteDeclaration, TRuntimeContext extends ContextShape = DefaultRuntimeContext, TRequest extends Request = DefaultRequest>(contract: TRoute): FetchRouteBuilder<TRoute, TRuntimeContext, TRequest>;
|
|
57
|
+
/**
|
|
58
|
+
* Creates a Fetch runtime router implementation builder for a contract tree.
|
|
59
|
+
*
|
|
60
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
61
|
+
*/
|
|
62
|
+
export declare function router<const TContract extends FetchContract, TRuntimeContext extends ContextShape = DefaultRuntimeContext, TRequest extends Request = DefaultRequest>(contract: TContract): FetchRouterBuilder<TContract, TRuntimeContext, TRequest>;
|
|
63
|
+
/**
|
|
64
|
+
* Creates a Fetch runtime `Request` handler from route implementations.
|
|
65
|
+
*
|
|
66
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
67
|
+
*/
|
|
68
|
+
export declare function createRouteHandler<TRuntimeContext extends ContextShape = DefaultRuntimeContext, TRequest extends Request = DefaultRequest>(implementations: FetchImplementationTree, options?: CreateFetchHandlerOptions): (request: TRequest, runtime: TRuntimeContext) => Promise<Response>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { clearCookie, RouteResponseError, setCookie, sseEvent, } from "@rest-rpc/server";
|
|
2
|
+
import { defaultParseBody, handleFetchRoute, } from "./http.js";
|
|
3
|
+
import { createFetchRouteMatcher } from "./match.js";
|
|
4
|
+
import { createFetchRouteBuilder, createFetchRouterBuilder, } from "./routeBuilder.js";
|
|
5
|
+
export { clearCookie, RouteResponseError, setCookie, sseEvent };
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Fetch runtime route implementation builder for a single HTTP route.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
10
|
+
*/
|
|
11
|
+
export function route(contract) {
|
|
12
|
+
return createFetchRouteBuilder(contract);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Creates a Fetch runtime router implementation builder for a contract tree.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
18
|
+
*/
|
|
19
|
+
export function router(contract) {
|
|
20
|
+
return createFetchRouterBuilder(contract);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a Fetch runtime `Request` handler from route implementations.
|
|
24
|
+
*
|
|
25
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
26
|
+
*/
|
|
27
|
+
export function createRouteHandler(implementations, options = {}) {
|
|
28
|
+
const matchRoute = createFetchRouteMatcher(implementations);
|
|
29
|
+
const usesDefaultParseBody = options.parseBody === undefined;
|
|
30
|
+
const parseBody = options.parseBody ?? defaultParseBody;
|
|
31
|
+
return async (request, runtime) => {
|
|
32
|
+
const match = matchRoute(request);
|
|
33
|
+
if (match instanceof Response)
|
|
34
|
+
return match;
|
|
35
|
+
const implementation = match.implementation;
|
|
36
|
+
let middlewareContext = {};
|
|
37
|
+
for (const middleware of implementation.middleware ?? []) {
|
|
38
|
+
const middlewareResult = await middleware({
|
|
39
|
+
context: middlewareContext,
|
|
40
|
+
request,
|
|
41
|
+
route: implementation.route,
|
|
42
|
+
runtime,
|
|
43
|
+
});
|
|
44
|
+
if (middlewareResult instanceof Response)
|
|
45
|
+
return middlewareResult;
|
|
46
|
+
middlewareContext = {
|
|
47
|
+
...middlewareContext,
|
|
48
|
+
...middlewareResult,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const context = {
|
|
52
|
+
...middlewareContext,
|
|
53
|
+
request,
|
|
54
|
+
};
|
|
55
|
+
return handleFetchRoute(request, context, implementation, match.params, parseBody, usesDefaultParseBody, options.errorHandlers);
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HttpRouteDeclaration } from "@rest-rpc/core/contract";
|
|
2
|
+
import { type ImplementationTree } from "@rest-rpc/server";
|
|
3
|
+
export declare const createFetchRouteMatcher: (implementations: ImplementationTree<HttpRouteDeclaration>) => (request: Request) => Response | {
|
|
4
|
+
type: "match";
|
|
5
|
+
implementation: import("@rest-rpc/server").RouteImplementation<HttpRouteDeclaration>;
|
|
6
|
+
params: Record<string, string>;
|
|
7
|
+
};
|
package/dist/match.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createRouteMatcher, flattenRouteImplementations, } from "@rest-rpc/server";
|
|
2
|
+
export const createFetchRouteMatcher = (implementations) => {
|
|
3
|
+
const routes = flattenRouteImplementations(implementations);
|
|
4
|
+
const routeContract = Object.fromEntries(routes.map((implementation, index) => [
|
|
5
|
+
String(index),
|
|
6
|
+
implementation.route,
|
|
7
|
+
]));
|
|
8
|
+
const matchRoute = createRouteMatcher(routeContract);
|
|
9
|
+
const implementationsByRoute = new Map(routes.map((implementation) => [implementation.route, implementation]));
|
|
10
|
+
return (request) => {
|
|
11
|
+
const url = new URL(request.url);
|
|
12
|
+
const match = matchRoute({
|
|
13
|
+
method: request.method,
|
|
14
|
+
path: url.pathname,
|
|
15
|
+
});
|
|
16
|
+
if (!match)
|
|
17
|
+
return new Response(null, { status: 404 });
|
|
18
|
+
if (match.type === "methodNotAllowed") {
|
|
19
|
+
return new Response(null, { status: 405 });
|
|
20
|
+
}
|
|
21
|
+
const implementation = implementationsByRoute.get(match.route);
|
|
22
|
+
if (!implementation)
|
|
23
|
+
return new Response(null, { status: 404 });
|
|
24
|
+
return {
|
|
25
|
+
type: "match",
|
|
26
|
+
implementation,
|
|
27
|
+
params: match.params,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { HttpRouteDeclaration } from "@rest-rpc/core/contract";
|
|
2
|
+
import { type Contract, type ImplementationTree, type ImplementationTreeFor, type RouteHandler, type RouteImplementation, type RouteHandlers as ServerRouteHandlers, type RouteRequest as ServerRouteRequest } from "@rest-rpc/server";
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
type Merge<T> = {
|
|
5
|
+
[K in keyof T]: T[K];
|
|
6
|
+
};
|
|
7
|
+
type MergeContext<TContext extends Record<string, unknown>, TNextContext extends Record<string, unknown>> = Merge<Omit<TContext, keyof TNextContext> & TNextContext>;
|
|
8
|
+
/**
|
|
9
|
+
* A contract tree containing only HTTP routes for the Fetch runtime.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
12
|
+
*/
|
|
13
|
+
export type FetchContract = Contract<HttpRouteDeclaration>;
|
|
14
|
+
/**
|
|
15
|
+
* An HTTP-only implementation tree for the Fetch runtime.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
18
|
+
*/
|
|
19
|
+
export type FetchImplementationTree = ImplementationTree<HttpRouteDeclaration>;
|
|
20
|
+
/**
|
|
21
|
+
* The context object passed to Fetch runtime route handlers.
|
|
22
|
+
*
|
|
23
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#framework-context}
|
|
24
|
+
*/
|
|
25
|
+
export type FetchRouteContext<TContext extends Record<string, unknown>, TRequest extends Request = Request> = Merge<TContext & {
|
|
26
|
+
request: TRequest;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Input passed to Fetch runtime middleware.
|
|
30
|
+
*
|
|
31
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#framework-context}
|
|
32
|
+
*/
|
|
33
|
+
export type FetchRouteMiddlewareInput<TRuntimeContext extends Record<string, unknown>, TRequest extends Request = Request, TContext extends Record<string, unknown> = Record<never, never>> = {
|
|
34
|
+
context: TContext;
|
|
35
|
+
request: TRequest;
|
|
36
|
+
route: HttpRouteDeclaration;
|
|
37
|
+
runtime: TRuntimeContext;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Result returned by Fetch runtime middleware.
|
|
41
|
+
*
|
|
42
|
+
* @remarks Returning a `Response` short-circuits the route handler.
|
|
43
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#framework-context}
|
|
44
|
+
*/
|
|
45
|
+
export type FetchRouteMiddlewareResult<TContext extends Record<string, unknown>> = Response | TContext | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Middleware function shape for Fetch runtime routes.
|
|
48
|
+
*
|
|
49
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch#framework-context}
|
|
50
|
+
*/
|
|
51
|
+
export type FetchRouteMiddleware<TRuntimeContext extends Record<string, unknown>, TContext extends Record<string, unknown>, TRequest extends Request = Request, TInputContext extends Record<string, unknown> = Record<never, never>> = (input: FetchRouteMiddlewareInput<TRuntimeContext, TRequest, TInputContext>) => MaybePromise<FetchRouteMiddlewareResult<TContext>>;
|
|
52
|
+
export type FetchRouteImplementation<TRuntimeContext extends Record<string, unknown>, TRequest extends Request = Request> = RouteImplementation<HttpRouteDeclaration> & {
|
|
53
|
+
middleware?: FetchRouteMiddleware<TRuntimeContext, Record<string, unknown>, TRequest, Record<string, unknown>>[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Infers the Fetch runtime route handler request type for a route declaration.
|
|
57
|
+
*
|
|
58
|
+
* @see {@link https://rest-rpc.dev/docs/type-helpers#server}
|
|
59
|
+
*/
|
|
60
|
+
export type RouteRequest<E extends HttpRouteDeclaration, TContext extends Record<string, unknown> = Record<string, unknown>, TRequest extends Request = Request> = ServerRouteRequest<E, FetchRouteContext<TContext, TRequest>>;
|
|
61
|
+
/**
|
|
62
|
+
* Handler tree accepted by `router().handlers()` when building a Fetch runtime implementation tree.
|
|
63
|
+
*
|
|
64
|
+
* @remarks Use this type with `implements` to check class-based route handler
|
|
65
|
+
* services against a contract tree.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* class TodoHandlers implements RouteHandlers<typeof api.todos> {
|
|
70
|
+
* get(request: RouteRequest<typeof api.todos.get>) {
|
|
71
|
+
* return { id: request.id };
|
|
72
|
+
* }
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @see {@link https://rest-rpc.dev/docs/recipes/organizing-route-handlers#service-classes-as-handlers}
|
|
77
|
+
*/
|
|
78
|
+
export type RouteHandlers<TContract extends FetchContract, TContext extends Record<string, unknown> = Record<string, unknown>, TRequest extends Request = Request> = ServerRouteHandlers<TContract, FetchRouteContext<TContext, TRequest>, Record<never, never>>;
|
|
79
|
+
type FetchRouteBuilderWithMiddleware<TRoute extends HttpRouteDeclaration, TContext extends Record<string, unknown>, TRequest extends Request> = {
|
|
80
|
+
handler(handler: RouteHandler<TRoute, FetchRouteContext<TContext, TRequest>>): RouteImplementation<TRoute> & FetchImplementationTree;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Builder for a single Fetch runtime route implementation.
|
|
84
|
+
*
|
|
85
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
86
|
+
*/
|
|
87
|
+
export type FetchRouteBuilder<TRoute extends HttpRouteDeclaration, TRuntimeContext extends Record<string, unknown>, TRequest extends Request = Request> = FetchRouteBuilderWithStackedMiddleware<TRoute, TRuntimeContext, Record<never, never>, TRequest>;
|
|
88
|
+
type FetchRouterBuilderWithMiddleware<TContract extends FetchContract, TContext extends Record<string, unknown>, TRequest extends Request> = {
|
|
89
|
+
handlers(handlers: RouteHandlers<TContract, TContext, TRequest>): ImplementationTreeFor<TContract, HttpRouteDeclaration> & FetchImplementationTree;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Builder for a Fetch runtime implementation tree.
|
|
93
|
+
*
|
|
94
|
+
* @see {@link https://rest-rpc.dev/docs/server/fetch}
|
|
95
|
+
*/
|
|
96
|
+
export type FetchRouterBuilder<TContract extends FetchContract, TRuntimeContext extends Record<string, unknown>, TRequest extends Request = Request> = FetchRouterBuilderWithMiddleware<TContract, Record<never, never>, TRequest> & FetchRouterBuilderWithStackedMiddleware<TContract, TRuntimeContext, Record<never, never>, TRequest>;
|
|
97
|
+
type FetchRouteBuilderWithStackedMiddleware<TRoute extends HttpRouteDeclaration, TRuntimeContext extends Record<string, unknown>, TContext extends Record<string, unknown>, TRequest extends Request> = FetchRouteBuilderWithMiddleware<TRoute, TContext, TRequest> & {
|
|
98
|
+
middleware<TNextContext extends Record<string, unknown>>(middleware: FetchRouteMiddleware<TRuntimeContext, TNextContext, TRequest, TContext>): FetchRouteBuilderWithStackedMiddleware<TRoute, TRuntimeContext, MergeContext<TContext, TNextContext>, TRequest>;
|
|
99
|
+
};
|
|
100
|
+
type FetchRouterBuilderWithStackedMiddleware<TContract extends FetchContract, TRuntimeContext extends Record<string, unknown>, TContext extends Record<string, unknown>, TRequest extends Request> = FetchRouterBuilderWithMiddleware<TContract, TContext, TRequest> & {
|
|
101
|
+
middleware<TNextContext extends Record<string, unknown>>(middleware: FetchRouteMiddleware<TRuntimeContext, TNextContext, TRequest, TContext>): FetchRouterBuilderWithStackedMiddleware<TContract, TRuntimeContext, MergeContext<TContext, TNextContext>, TRequest>;
|
|
102
|
+
};
|
|
103
|
+
export declare const createFetchRouteBuilder: <const TRoute extends HttpRouteDeclaration, TRuntimeContext extends Record<string, unknown>, TRequest extends Request = Request, TContext extends Record<string, unknown> = Record<never, never>>(contract: TRoute, middlewares?: FetchRouteMiddleware<TRuntimeContext, Record<string, unknown>, TRequest, Record<string, unknown>>[]) => FetchRouteBuilder<TRoute, TRuntimeContext, TRequest> & FetchRouteBuilderWithStackedMiddleware<TRoute, TRuntimeContext, TContext, TRequest>;
|
|
104
|
+
export declare const createFetchRouterBuilder: <const TContract extends FetchContract, TRuntimeContext extends Record<string, unknown>, TRequest extends Request = Request, TContext extends Record<string, unknown> = Record<never, never>>(contract: TContract, middlewares?: FetchRouteMiddleware<TRuntimeContext, Record<string, unknown>, TRequest, Record<string, unknown>>[]) => FetchRouterBuilder<TContract, TRuntimeContext, TRequest> & FetchRouterBuilderWithStackedMiddleware<TContract, TRuntimeContext, TContext, TRequest>;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { route as serverRoute, router as serverRouter, } from "@rest-rpc/server";
|
|
2
|
+
const attachMiddleware = (implementation, middleware) => {
|
|
3
|
+
if ("route" in implementation && "handler" in implementation) {
|
|
4
|
+
return {
|
|
5
|
+
...implementation,
|
|
6
|
+
middleware,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
return Object.fromEntries(Object.entries(implementation).map(([key, child]) => [
|
|
10
|
+
key,
|
|
11
|
+
attachMiddleware(child, middleware),
|
|
12
|
+
]));
|
|
13
|
+
};
|
|
14
|
+
export const createFetchRouteBuilder = (contract, middlewares = []) => ({
|
|
15
|
+
middleware: (nextMiddleware) => createFetchRouteBuilder(contract, [
|
|
16
|
+
...middlewares,
|
|
17
|
+
nextMiddleware,
|
|
18
|
+
]),
|
|
19
|
+
handler: (handler) => attachMiddleware(serverRoute(contract, handler), middlewares),
|
|
20
|
+
});
|
|
21
|
+
export const createFetchRouterBuilder = (contract, middlewares = []) => ({
|
|
22
|
+
middleware: (nextMiddleware) => createFetchRouterBuilder(contract, [
|
|
23
|
+
...middlewares,
|
|
24
|
+
nextMiddleware,
|
|
25
|
+
]),
|
|
26
|
+
handlers: (handlers) => serverRouter(contract, handlers, {
|
|
27
|
+
createRouteImplementation: ({ route, handler }) => attachMiddleware({ route: route, handler }, middlewares),
|
|
28
|
+
}),
|
|
29
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rest-rpc/fetch",
|
|
3
|
+
"version": "0.1.0-beta.14",
|
|
4
|
+
"description": "Fetch runtime adapter for serving rest-rpc contracts with Request and Response handlers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"api-contract",
|
|
7
|
+
"contract-first",
|
|
8
|
+
"edge",
|
|
9
|
+
"fetch",
|
|
10
|
+
"http",
|
|
11
|
+
"request",
|
|
12
|
+
"response",
|
|
13
|
+
"rest",
|
|
14
|
+
"rest-rpc",
|
|
15
|
+
"rpc",
|
|
16
|
+
"serverless",
|
|
17
|
+
"type-safe-api",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/rest-rpc/rest-rpc.git",
|
|
24
|
+
"directory": "packages/fetch"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@rest-rpc/core": "0.1.0-beta.14",
|
|
40
|
+
"@rest-rpc/server": "0.1.0-beta.14"
|
|
41
|
+
},
|
|
42
|
+
"tsd": {
|
|
43
|
+
"directory": "test-d"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"test": "node --test src/**/*.test.ts",
|
|
48
|
+
"typecheck": "pnpm run build && tsc -p tsconfig.json && tsd"
|
|
49
|
+
}
|
|
50
|
+
}
|