-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Ideas (big pic inspired by Hono RPC):
Package @ty-ras/rest-api (new)
Something like this
export type APIInfoGeneric<TValidator> = Record<string, SingleAPIInfoGeneric<TValidator>>;
export type SingleAPIInfoGeneric<TValidator> = SingleAPIInfoNamedGeneric<TValidator> &
SingleAPIInfoWithMethodsGeneric<TValidator>;
export interface SingleAPIInfoNamedGeneric<TValidator> {
[key: string]: SingleAPIInfoGeneric<TValidator>;
}
export type SingleAPIInfoWithMethodsGeneric<TValidator> = Partial<
Record<HTTPMethodSymbols, EndpointInfoGeneric<TValidator>>
>;
export type HTTPMethodSymbols = typeof GET | typeof POST;
export type EndpointInfoGeneric<TValidator> = EndpointInfoResponseBodyGeneric<TValidator> &
EndpointInfoQueryGeneric<TValidator> &
EndpointInfoRequestHeadersDataGeneric<TValidator> &
EndpointInfoResponseHeadersDataGeneric<TValidator>;
export interface EndpointInfoResponseBodyGeneric<TValidator> {
responseBody: TValidator;
}
export interface EndpointInfoURLGeneric<TValidator> {
url?: Record<string, TValidator>;
}
export interface EndpointInfoQueryGeneric<TValidator> {
query?: Record<string, TValidator>;
}
export interface EndpointInfoRequestHeadersDataGeneric<TValidator> {
requestHeaders?: Record<string, TValidator>;
}
export interface EndpointInfoResponseHeadersDataGeneric<TValidator> {
responseHeaders?: Record<string, TValidator>;
}
export const GET = Symbol('GET');
export const POST = Symbol('POST');And then
export const testing = {
test: {
another: {
[GET]: {
responseBody: 'validator',
},
},
[POST]: {
responseBody: 'validator',
},
},
} as const satisfies APIInfoGeneric<string>;Package @ty-ras/rest-api-io-ts (new)
Has
import type * as endpoints from '@ty-ras/rest-api';
import type * as t from 'io-ts';
export type AnyValidator = t.Any;
export type APIInfo = endpoints.APIInfoGeneric<AnyValidator>;
export type SingleAPIInfoGeneric = endpoints.SingleAPIInfoGeneric<AnyValidator>;
export type SingleAPIInfoNamed = endpoints.SingleAPIInfoNamedGeneric<AnyValidator>;
export type SingleAPIInfoWithMethods = endpoints.SingleAPIInfoWithMethodsGeneric<AnyValidator>;
export type EndpointInfo = endpoints.EndpointInfoGeneric<AnyValidator>;
export type EndpointInfoResponseBody = endpoints.EndpointInfoResponseBodyGeneric<AnyValidator>;
export type EndpointInfoURL = endpoints.EndpointInfoURLGeneric<AnyValidator>;
export type EndpointInfoQuery = endpoints.EndpointInfoQueryGeneric<AnyValidator>;
export type EndpointInfoRequestHeadersData =
endpoints.EndpointInfoRequestHeadersDataGeneric<AnyValidator>;
export type EndpointInfoResponseHeadersData =
endpoints.EndpointInfoResponseHeadersDataGeneric<AnyValidator>;Consequentially, then
export const testing = {
test: {
another: {
[endpoints.GET]: {
responseBody: t.string,
},
},
[endpoints.POST]: {
responseBody: t.boolean,
},
},
} as const satisfies APIInfo;Package @ty-ras/endpoint-spec (pre-existing)
The ApplicationBuilderGeneric would need to have something like
createEndpoints: <TEndpoints extends [EndpointCreationArg, ...Array<EndpointCreationArg>]>(
this: void,
mdArgs: {
[P in keyof TMetadataProviders]: md.MaterializeParameterWhenCreatingEndpoints<
TMetadataProviders[P]
>;
},
...endpoints: TEndpoints
) => EndpointsCreationResult<
TMetadataProviders,
TServerContextArg,
dataBE.MaterializeStateInfo<
TStateHKT,
dataBE.MaterializeStateSpecBase<TStateHKT>
>,
InferAPIInfoType<TEndpoints> // <-- new generic parameter!
>;Backend builds application, which is runnable + exposes object which is as const satisfies APIInfo.
This object is then used by FE to build full client.
Args for when FE building client:
- URL prefix
- Callback to get (auth) headers
Notice that this particular scenario we would end up importing Node things into FE package.
Because we want validation (= transformation) to also happen on FE end (e.g. to handle Date <-> string automatic translation by validation framework like io-ts and more recently, zod), we must get the validation objects to FE also (unlike Hono RPC which gets only typings, and has no runtime validation on FE side).
We might end up with something like this
// spec.ts, shared between BE and FE
import type * as tyras from "@ty-ras/rest-api-io-ts";
import * as t from "io-ts";
export default {
test: {
another: {
[tyras.GET]: {
responseBody: t.string,
},
},
[tyras.POST]: {
responseBody: t.boolean,
},
},
} as const satisfies tyras.APIInfo;// backend.ts, BE-only
import * as tyras from "@ty-ras/backend-node-io-ts-openapi";
import spec from "./spec";
export const app = tyras.buildApp(
spec,
{
test: {
another: {
[tyras.GET]: (args) => implementation,
},
[tyras.POST]: (args) => implementation,
},
}
);// frontend.ts, FE-only
import * as tyras from "@ty-ras/frontend-fetch-io-ts";
import spec from "@app/backend/spec";
export const api = tyras.buildClient(
spec,
[ urlPrefix = "/prefix" ],
);
// Usage:
const stringResult = await api.test.another[tyras.GET](args);This makes #72 obsolete