Skip to content

Instantly share code, notes, and snippets.

@emeraldsanto
Last active April 28, 2022 20:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emeraldsanto/b25c4d1055a4e7149fe54e0d9a75741f to your computer and use it in GitHub Desktop.
Save emeraldsanto/b25c4d1055a4e7149fe54e0d9a75741f to your computer and use it in GitHub Desktop.
Define routes according to the Open API / Swagger specification and get a fully typed express resolver.
// === Parameter definition ===
interface RouteParameterBase {
description?: string;
nullable?: boolean;
required?: boolean;
}
interface BooleanRouteParameter extends RouteParameterBase {
type: 'boolean';
default?: 'true' | 'false';
}
interface StringRouteParameter extends RouteParameterBase {
default?: string;
enum?: Array<string> | ReadonlyArray<string>;
format?:
| 'binary'
| 'byte'
| 'date-time'
| 'date'
| 'email'
| 'hostname'
| 'ipv4'
| 'ipv6'
| 'password'
| 'uri'
| 'uuid';
pattern?: string;
type: 'string';
}
interface IntegerRouteParameter extends RouteParameterBase {
default?: number;
format?: 'int32' | 'int64';
maximum?: number;
minimum?: number;
type: 'integer';
}
interface NumberRouteParameter extends RouteParameterBase {
default?: number;
format?: 'double' | 'float';
maximum?: number;
minimum?: number;
type: 'number';
}
interface ObjectRouteParameter extends RouteParameterBase {
properties: Record<string, NestedRouteParameter>;
type: 'object';
}
interface ArrayRouteParameter extends RouteParameterBase {
items: NestedRouteParameter;
type: 'array';
}
type NestedRouteParameter =
| BooleanRouteParameter
| StringRouteParameter
| IntegerRouteParameter
| NumberRouteParameter
| ObjectRouteParameter
| ArrayRouteParameter;
type ParameterLocation = 'body' | 'cookie' | 'headers' | 'path' | 'query';
type RouteParameter<
TName extends string = string,
TLocation extends ParameterLocation = ParameterLocation
> = NestedRouteParameter & {
in: TLocation;
name: TName;
};
// === Route definition ===
import { Request, RequestHandler } from 'express';
interface RouteDefinition<TResponse, TParameters extends Array<RouteParameter>> {
description?: string;
method?: 'get' | 'post' | 'patch' | 'put' | 'delete';
middlewares?: Array<RequestHandler>;
operationId: string;
parameters?: TParameters;
path: string;
resolver: (
req: Request<
ParametersIn<'cookie' | 'headers' | 'path', TParameters>,
Record<string, never>,
ParametersIn<'body', TParameters>,
ParametersIn<'query', TParameters>
>
) => TResponse;
tags?: string[];
}
// === Utility types ===
type RuntimeType<TParameter extends NestedRouteParameter | RouteParameter> = TParameter extends StringRouteParameter
? string
: TParameter extends BooleanRouteParameter
? boolean
: TParameter extends NumberRouteParameter
? number
: TParameter extends ObjectRouteParameter
? { [key in keyof TParameter['properties']]: RuntimeType<TParameter['properties'][key]> }
: TParameter extends ArrayRouteParameter
? Array<RuntimeType<TParameter['items']>>
: never;
type ParametersIn<TLocation extends ParameterLocation, TParameters extends Array<RouteParameter>> = {
[TParameter in TParameters[number] as TParameter['name']]: TParameter['in'] extends TLocation
? RuntimeType<TParameter>
: never;
};
// === Identity functions ===
declare function parameter<
TName extends string,
TLocation extends ParameterLocation,
T extends RouteParameter<TName, TLocation>
>(x: T): T;
declare function register<TResponse, TParameters extends Array<RouteParameter>>(
route: RouteDefinition<TResponse, TParameters>
): RouteDefinition<TResponse, TParameters>;
// === Test cases ===
register({
operationId: 'test',
path: '/',
parameters: [
parameter({
name: 'foo',
in: 'body',
type: 'object',
properties: {
bar: {
type: 'array',
items: {
type: 'number'
}
}
},
}),
parameter({ name: 'baz', in: 'path', type: 'string' }),
],
resolver(ctx) {
ctx.body.foo; // { bar: Array<number> }
ctx.params.baz; // string
return 'Hello world';
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment