Skip to content

v3 notes #87

@stazz

Description

@stazz

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions