@noodletired/rest-ts-core
Version:
Type-safe rest API definitions to share
335 lines (334 loc) • 21.6 kB
TypeScript
/**
* @module rest-ts-core
*/
import { AEndpointBuilder, HttpMethod, ApiDefinition, EndpointDefinition, SafeQueryParams, QueryParams } from './types';
import { RemoveKey } from './private/ts-kung-fu';
/**
* Builder class to create endpoint definitions.
*
* This class provides a high-level user friendly interface to create a typed definition of an API endpoint.
*
* Endpoint definitions are usually grouped together in API definitions, which can be shared between producers
* and consumers of the API to create a type-safe communication channel.
*
* You will generally not need to create this class explicitly. Instead, use the helper methods [[GET]], [[PUT]],
* [[PATCH]], [[POST]] or [[DELETE]] to create an instance of EndpointBuilder.
*/
export declare class EndpointBuilder<T extends Partial<EndpointDefinition>> implements AEndpointBuilder<T> {
readonly def: Readonly<T>;
constructor(def: Readonly<T>);
/**
* Change the HTTP method of this endpoint
* @param method Any valid HTTP method. See [[HttpMethod]].
*/
method<U extends HttpMethod>(method: U): EndpointBuilder<RemoveKey<T, 'method'> & {
method: U;
}>;
/**
* Change the type of the response.
*
* You must provide a real object whose type will be inferred and used as the response type for this endpoint.
* Rest.ts offers multiple ways to do this:
*
* 1. Use a regular object.
* For instance, if you need a string, use `'string'`, if you need a number, use `123`.
* This also works for complex objects like so:
*
* GET `/current-user`
* .response({
* id: 'string',
* kind: 'person' as 'person' | 'robot' | 'cat'
* })
*
* 2. Some people like to use classes to define their DTOs. If this is your situation, you may just put the
* class constructor here, and the instance type will be inferred.
*
* class CurrentUserDTO {
* id: string;
* kind: 'person' | 'robot' | 'cat';
* }
*
* GET `/current-user`
* .response(CurrentUserDTO)
*
* 3. **(Preferred method)** Use a Runtype. [runtypes](https://github.com/pelotom/runtypes) is a
* library that allows you to create type definitions with runtime type metadata to ensure that
* input data conforms to an expected type.
* Rest.ts has first-class support for runtypes:
*
* const CurrentUserDTO = rt.Record({
* id: rt.String,
* kind: rt.Union(
* rt.String('person'),
* rt.String('robot'),
* rt.String('cat')
* )
* });
*
* GET `/current-user`
* .response(CurrentUserDTO)
*
* @param response type of the response data.
*/
response<U extends EndpointDefinition['response']>(response: U): EndpointBuilder<RemoveKey<T, 'response'> & {
response: U;
}>;
/**
* Change the type of the request body.
*
* You must provide a real object whose type will be inferred and used as the response type for this endpoint.
* Rest.ts offers multiple ways to do this:
*
* 1. Use a regular object.
* For instance, if you need a string, use `'string'`, if you need a number, use `123`.
* This also works for complex objects like so:
*
* POST `/current-user`
* .body({
* id: 'string',
* kind: 'person' as 'person' | 'robot' | 'cat'
* })
*
* 2. Some people like to use classes to define their DTOs. If this is your situation, you may just put the
* class constructor here, and the instance type will be inferred.
*
* class CurrentUserDTO {
* id: string;
* kind: 'person' | 'robot' | 'cat';
* }
*
* POST `/current-user`
* .body(CurrentUserDTO)
*
* 3. **(Preferred method)** Use a Runtype. [runtypes](https://github.com/pelotom/runtypes) is a
* library that allows you to create type definitions with runtime type metadata to ensure that
* input data conforms to an expected type.
* Rest.ts has first-class support for runtypes:
*
* const CurrentUserDTO = rt.Record({
* id: rt.String,
* kind: rt.Union(
* rt.String('person'),
* rt.String('robot'),
* rt.String('cat')
* )
* });
*
* POST `/current-user`
* .body(CurrentUserDTO)
*
* rest-ts-express automatically type-checks incoming data when the body of the endpoint definition is a runtype.
*
* @param response type of the response data.
*/
body<U extends EndpointDefinition['body']>(body: U): EndpointBuilder<RemoveKey<T, 'body'> & {
body: U;
}>;
/**
* Add query parameters.
*
* Note that query parameters are always optional.
*
* Example:
*
* GET `/users/search`
* .query({
* 'order': 'string',
* 'filter': 'string'
* })
*
* @param query type of the query parameters.
*/
// @ts-ignore
query<U extends QueryParams>(query: SafeQueryParams<U>): EndpointBuilder<RemoveKey<T, 'query'> & {
query: SafeQueryParams<U>;
}>;
}
export interface InitialEndpointDefinition<Params, METHOD extends HttpMethod | undefined> {
path: string[];
params: Params;
method: METHOD;
response: undefined;
}
export interface EmptyInitialEndpointDefinition<METHOD extends HttpMethod | undefined> {
path: string[];
method: METHOD;
response: undefined;
}
/**
* Create a GET endpoint definition.
*
* Use of the template literal allows to easily add dynamic path parameters to the endpoint definition, as shown in the example below.
*
* Use as a tagged template literal to add path parameters to the endpoint, and use methods of
* the [[EndpointBuilder]] class to customize the endpoint definition.
*
* This endpoint definition can be consumed by API servers and clients such as rest-ts-express and rest-ts-axios.
*
* Example:
*
* export const carsAPI = defineAPI({
* // Get emissions test results for a given car.
* // For example: GET /cars/VW_Golf_TDI/results => "OK"
* getCarTestResults: GET `/cars/${'model'}/results`
* .response(CarTestResults)
* });
*/
export declare function GET(path: string | TemplateStringsArray): EndpointBuilder<EmptyInitialEndpointDefinition<'GET'>>;
export declare function GET<A extends string>(strings: TemplateStringsArray, a: A): EndpointBuilder<InitialEndpointDefinition<[A], 'GET'>>;
export declare function GET<A extends string, B extends string>(strings: TemplateStringsArray, a: A, b: B): EndpointBuilder<InitialEndpointDefinition<[A, B], 'GET'>>;
export declare function GET<A extends string, B extends string, C extends string>(strings: TemplateStringsArray, a: A, b: B, c: C): EndpointBuilder<InitialEndpointDefinition<[A, B, C], 'GET'>>;
export declare function GET<A extends string, B extends string, C extends string, D extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D], 'GET'>>;
export declare function GET<A extends string, B extends string, C extends string, D extends string, E extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E], 'GET'>>;
export declare function GET<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F], 'GET'>>;
export declare function GET<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G], 'GET'>>;
export declare function GET<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string, H extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G, H], 'GET'>>;
/**
* Create a PUT endpoint definition.
*
* Use of the template literal allows to easily add dynamic path parameters to the endpoint definition, as shown in the example below.
*
* Use as a tagged template literal to add path parameters to the endpoint, and use methods of
* the [[EndpointBuilder]] class to customize the endpoint definition.
*
* This endpoint definition can be consumed by API servers and clients such as rest-ts-express and rest-ts-axios.
*
* Example:
*
* export const carsAPI = defineAPI({
* // Edit a car in the list
* // For example: PUT /cars/123/edit
* editCar: PUT `/cars/${'id'}/edit`
* .body(CarAttributes)
* .response(CarSaveResponse)
* });
*/
export declare function PUT(path: string | TemplateStringsArray): EndpointBuilder<EmptyInitialEndpointDefinition<'PUT'>>;
export declare function PUT<A extends string>(strings: TemplateStringsArray, a: A): EndpointBuilder<InitialEndpointDefinition<[A], 'PUT'>>;
export declare function PUT<A extends string, B extends string>(strings: TemplateStringsArray, a: A, b: B): EndpointBuilder<InitialEndpointDefinition<[A, B], 'PUT'>>;
export declare function PUT<A extends string, B extends string, C extends string>(strings: TemplateStringsArray, a: A, b: B, c: C): EndpointBuilder<InitialEndpointDefinition<[A, B, C], 'PUT'>>;
export declare function PUT<A extends string, B extends string, C extends string, D extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D], 'PUT'>>;
export declare function PUT<A extends string, B extends string, C extends string, D extends string, E extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E], 'PUT'>>;
export declare function PUT<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F], 'PUT'>>;
export declare function PUT<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G], 'PUT'>>;
export declare function PUT<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string, H extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G, H], 'PUT'>>;
/**
* Create a POST endpoint definition.
*
* Use of the template literal allows to easily add dynamic path parameters to the endpoint definition, as shown in the example below.
*
* Use as a tagged template literal to add path parameters to the endpoint, and use methods of
* the [[EndpointBuilder]] class to customize the endpoint definition.
*
* This endpoint definition can be consumed by API servers and clients such as rest-ts-express and rest-ts-axios.
*
* Example:
*
* export const commentsAPI = defineAPI({
* // Add a comment to an article
* // For example: POST /article/123/comment
* addComment: POST `/article/${'id'}/comment`
* .body(CommentAttributes)
* .response(CommentSaveResponse)
* });
*/
export declare function POST(path: string | TemplateStringsArray): EndpointBuilder<EmptyInitialEndpointDefinition<'POST'>>;
export declare function POST<A extends string>(strings: TemplateStringsArray, a: A): EndpointBuilder<InitialEndpointDefinition<[A], 'POST'>>;
export declare function POST<A extends string, B extends string>(strings: TemplateStringsArray, a: A, b: B): EndpointBuilder<InitialEndpointDefinition<[A, B], 'POST'>>;
export declare function POST<A extends string, B extends string, C extends string>(strings: TemplateStringsArray, a: A, b: B, c: C): EndpointBuilder<InitialEndpointDefinition<[A, B, C], 'POST'>>;
export declare function POST<A extends string, B extends string, C extends string, D extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D], 'POST'>>;
export declare function POST<A extends string, B extends string, C extends string, D extends string, E extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E], 'POST'>>;
export declare function POST<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F], 'POST'>>;
export declare function POST<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G], 'POST'>>;
export declare function POST<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string, H extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G, H], 'POST'>>;
/**
* Create a DELETE endpoint definition.
*
* Use of the template literal allows to easily add dynamic path parameters to the endpoint definition, as shown in the example below.
*
* Use as a tagged template literal to add path parameters to the endpoint, and use methods of
* the [[EndpointBuilder]] class to customize the endpoint definition.
*
* This endpoint definition can be consumed by API servers and clients such as rest-ts-express and rest-ts-axios.
*
* Example:
*
* export const commentsAPI = defineAPI({
* // Remove a comment
* // For example: DELETE /comments/123
* removeComment: DELETE `/comments/${'id'}`
* .response(CommentDeleteResponse)
* });
*/
export declare function DELETE(path: string | TemplateStringsArray): EndpointBuilder<EmptyInitialEndpointDefinition<'DELETE'>>;
export declare function DELETE<A extends string>(strings: TemplateStringsArray, a: A): EndpointBuilder<InitialEndpointDefinition<[A], 'DELETE'>>;
export declare function DELETE<A extends string, B extends string>(strings: TemplateStringsArray, a: A, b: B): EndpointBuilder<InitialEndpointDefinition<[A, B], 'DELETE'>>;
export declare function DELETE<A extends string, B extends string, C extends string>(strings: TemplateStringsArray, a: A, b: B, c: C): EndpointBuilder<InitialEndpointDefinition<[A, B, C], 'DELETE'>>;
export declare function DELETE<A extends string, B extends string, C extends string, D extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D], 'DELETE'>>;
export declare function DELETE<A extends string, B extends string, C extends string, D extends string, E extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E], 'DELETE'>>;
export declare function DELETE<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F], 'DELETE'>>;
export declare function DELETE<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G], 'DELETE'>>;
export declare function DELETE<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string, H extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G, H], 'DELETE'>>;
/**
* Create a PATCH endpoint definition.
*
* Use of the template literal allows to easily add dynamic path parameters to the endpoint definition, as shown in the example below.
*
* Use as a tagged template literal to add path parameters to the endpoint, and use methods of
* the [[EndpointBuilder]] class to customize the endpoint definition.
*
* This endpoint definition can be consumed by API servers and clients such as rest-ts-express and rest-ts-axios.
*
* Example:
*
* export const commentsAPI = defineAPI({
* // Edit a comment
* // For example: PATCH /article/123/comment/2
* editComment: PATCH `/article/${'id'}/comment/${'commentId'}`
* .body(CommentAttributes)
* .response(CommentSaveResponse)
* });
*/
export declare function PATCH(path: string | TemplateStringsArray): EndpointBuilder<EmptyInitialEndpointDefinition<'PATCH'>>;
export declare function PATCH<A extends string>(strings: TemplateStringsArray, a: A): EndpointBuilder<InitialEndpointDefinition<[A], 'PATCH'>>;
export declare function PATCH<A extends string, B extends string>(strings: TemplateStringsArray, a: A, b: B): EndpointBuilder<InitialEndpointDefinition<[A, B], 'PATCH'>>;
export declare function PATCH<A extends string, B extends string, C extends string>(strings: TemplateStringsArray, a: A, b: B, c: C): EndpointBuilder<InitialEndpointDefinition<[A, B, C], 'PATCH'>>;
export declare function PATCH<A extends string, B extends string, C extends string, D extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D], 'PATCH'>>;
export declare function PATCH<A extends string, B extends string, C extends string, D extends string, E extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E], 'PATCH'>>;
export declare function PATCH<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F], 'PATCH'>>;
export declare function PATCH<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G], 'PATCH'>>;
export declare function PATCH<A extends string, B extends string, C extends string, D extends string, E extends string, F extends string, G extends string, H extends string>(strings: TemplateStringsArray, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): EndpointBuilder<InitialEndpointDefinition<[A, B, C, D, E, F, G, H], 'PATCH'>>;
/**
* Create an API definition to share across producers and consumers of the API.
*
* The usual workflow of rest-ts-core goes like this:
*
* 1. Create an API definition:
*
* const myAwesomeAPI = defineAPI({
* someEndpoint: GET `/some/path`
* .response(SomeResponseDTO)
* });
*
* 2. Create a server for this API.
* rest-ts-express allows you to import the API definition you just created and
* turn it into an express router. See the documentation for that package for more details.
*
* 3. Create a consumer for this API.
* rest-ts-axios lets you create a typed instance of [axios](https://github.com/axios/axios) to
* perform requests to your API.
*
* 4. ... Profit!
*
*
* Notice: Unless you are authoring an adapter for Rest.ts, you should always treat the return type of this function
* as an opaque type. Use the utilities provided by this library to create the API definition within the brackets
* of `defineAPI({ ... })`, and export the resulting symbol to be consumed by your server and client(s).
* The type you get from `defineAPI` is very complex, and for a good reason: it encodes all of the type information
* of your API! It is pointless to inspect the raw type you get. Instead, we recommend that you feed it directly
* to a compatible binding library such as rest-ts-express and rest-ts-axios. These libraries are able to decode
* the complex type and make sense out of it.
*
* @param api
*/
export declare const defineAPI: <T extends ApiDefinition>(api: T) => T;