Skip to content

Commit 85d66a9

Browse files
committed
feat(express-wrapper): allow custom response encoders
1 parent fc40a73 commit 85d66a9

File tree

3 files changed

+100
-71
lines changed

3 files changed

+100
-71
lines changed

packages/express-wrapper/src/index.ts

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,53 @@ import {
1414
getServiceFunction,
1515
RouteHandler,
1616
} from './request';
17+
import { defaultResponseEncoder, ResponseEncoder } from './response';
18+
19+
export type { ResponseEncoder } from './response';
1720

1821
const isHttpVerb = (verb: string): verb is 'get' | 'put' | 'post' | 'delete' =>
1922
verb === 'get' || verb === 'put' || verb === 'post' || verb === 'delete';
2023

21-
export function createServer<Spec extends ApiSpec>(
22-
spec: Spec,
23-
configureExpressApplication: (app: express.Application) => {
24-
[ApiName in keyof Spec]: {
25-
[Method in keyof Spec[ApiName]]: RouteHandler<Spec[ApiName][Method]>;
26-
};
27-
},
28-
) {
29-
const app: express.Application = express();
30-
const routes = configureExpressApplication(app);
31-
32-
const router = express.Router();
33-
for (const apiName of Object.keys(spec)) {
34-
const resource = spec[apiName] as Spec[string];
35-
for (const method of Object.keys(resource)) {
36-
if (!isHttpVerb(method)) {
37-
continue;
24+
export const createServerWithResponseEncoder =
25+
(encoder: ResponseEncoder) =>
26+
<Spec extends ApiSpec>(
27+
spec: ApiSpec,
28+
configureExpressApplication: (app: express.Application) => {
29+
[ApiName in keyof Spec]: {
30+
[Method in keyof Spec[ApiName]]: RouteHandler<Spec[ApiName][Method]>;
31+
};
32+
},
33+
) => {
34+
const app: express.Application = express();
35+
const routes = configureExpressApplication(app);
36+
37+
const router = express.Router();
38+
for (const apiName of Object.keys(spec)) {
39+
const resource = spec[apiName] as Spec[string];
40+
for (const method of Object.keys(resource)) {
41+
if (!isHttpVerb(method)) {
42+
continue;
43+
}
44+
const httpRoute: HttpRoute = resource[method]!;
45+
const routeHandler = routes[apiName]![method]!;
46+
const expressRouteHandler = decodeRequestAndEncodeResponse(
47+
apiName,
48+
httpRoute,
49+
// FIXME: TS is complaining that `routeHandler` is not necessarily guaranteed to be a
50+
// `ServiceFunction`, because subtypes of Spec[string][string] can have arbitrary extra keys.
51+
getServiceFunction(routeHandler as any),
52+
encoder,
53+
);
54+
const handlers = [...getMiddleware(routeHandler), expressRouteHandler];
55+
56+
const expressPath = apiTsPathToExpress(httpRoute.path);
57+
router[method](expressPath, handlers);
3858
}
39-
const httpRoute: HttpRoute = resource[method]!;
40-
const routeHandler = routes[apiName]![method]!;
41-
const expressRouteHandler = decodeRequestAndEncodeResponse(
42-
apiName,
43-
httpRoute as any, // TODO: wat
44-
getServiceFunction(routeHandler),
45-
);
46-
const handlers = [...getMiddleware(routeHandler), expressRouteHandler];
47-
48-
const expressPath = apiTsPathToExpress(httpRoute.path);
49-
router[method](expressPath, handlers);
5059
}
51-
}
5260

53-
app.use(router);
61+
app.use(router);
62+
63+
return app;
64+
};
5465

55-
return app;
56-
}
66+
export const createServer = createServerWithResponseEncoder(defaultResponseEncoder);

packages/express-wrapper/src/request.ts

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,11 @@
44
*/
55

66
import express from 'express';
7-
import * as t from 'io-ts';
87
import * as PathReporter from 'io-ts/lib/PathReporter';
98

10-
import {
11-
HttpRoute,
12-
HttpToKeyStatus,
13-
KeyToHttpStatus,
14-
RequestType,
15-
ResponseType,
16-
} from '@api-ts/io-ts-http';
9+
import { HttpRoute, RequestType } from '@api-ts/io-ts-http';
1710

18-
type NumericOrKeyedResponseType<R extends HttpRoute> =
19-
| ResponseType<R>
20-
| {
21-
[S in keyof R['response']]: S extends keyof HttpToKeyStatus
22-
? {
23-
type: HttpToKeyStatus[S];
24-
payload: t.TypeOf<R['response'][S]>;
25-
}
26-
: never;
27-
}[keyof R['response']];
11+
import type { NumericOrKeyedResponseType, ResponseEncoder } from './response';
2812

2913
export type ServiceFunction<R extends HttpRoute> = (
3014
input: RequestType<R>,
@@ -53,10 +37,11 @@ const createNamedFunction = <F extends (...args: any) => void>(
5337
fn: F,
5438
): F => Object.defineProperty(fn, 'name', { value: name });
5539

56-
export const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
40+
export const decodeRequestAndEncodeResponse = (
5741
apiName: string,
58-
httpRoute: Route,
59-
handler: ServiceFunction<Route>,
42+
httpRoute: HttpRoute,
43+
handler: ServiceFunction<HttpRoute>,
44+
responseEncoder: ResponseEncoder,
6045
): express.RequestHandler => {
6146
return createNamedFunction(
6247
'decodeRequestAndEncodeResponse' + httpRoute.method + apiName,
@@ -72,7 +57,7 @@ export const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
7257
return;
7358
}
7459

75-
let rawResponse: NumericOrKeyedResponseType<Route> | undefined;
60+
let rawResponse: NumericOrKeyedResponseType<HttpRoute> | undefined;
7661
try {
7762
rawResponse = await handler(maybeRequest.right);
7863
} catch (err) {
@@ -81,23 +66,7 @@ export const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
8166
return;
8267
}
8368

84-
const { type, payload } = rawResponse;
85-
const status = typeof type === 'number' ? type : (KeyToHttpStatus as any)[type];
86-
if (status === undefined) {
87-
console.warn('Unknown status code returned');
88-
res.status(500).end();
89-
return;
90-
}
91-
const responseCodec = httpRoute.response[status];
92-
if (responseCodec === undefined || !responseCodec.is(payload)) {
93-
console.warn(
94-
"Unable to encode route's return value, did you return the expected type?",
95-
);
96-
res.status(500).end();
97-
return;
98-
}
99-
100-
res.status(status).json(responseCodec.encode(payload)).end();
69+
responseEncoder(httpRoute, rawResponse, res);
10170
},
10271
);
10372
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import express from 'express';
2+
import * as t from 'io-ts';
3+
4+
import {
5+
HttpRoute,
6+
HttpToKeyStatus,
7+
KeyToHttpStatus,
8+
ResponseType,
9+
} from '@api-ts/io-ts-http';
10+
11+
export type NumericOrKeyedResponseType<R extends HttpRoute> =
12+
| ResponseType<R>
13+
| {
14+
[Key in keyof R['response'] & keyof HttpToKeyStatus]: {
15+
type: HttpToKeyStatus[Key];
16+
payload: t.TypeOf<R['response'][Key]>;
17+
};
18+
}[keyof R['response'] & keyof HttpToKeyStatus];
19+
20+
// TODO: Use HKT (using fp-ts or a similar workaround method, or who knows maybe they'll add
21+
// official support) to allow for polymorphic ResponseType<_>.
22+
export type ResponseEncoder = (
23+
route: HttpRoute,
24+
serviceFnResponse: NumericOrKeyedResponseType<HttpRoute>,
25+
expressRes: express.Response,
26+
) => void;
27+
28+
export const defaultResponseEncoder: ResponseEncoder = (
29+
route,
30+
serviceFnResponse,
31+
expressRes,
32+
) => {
33+
const { type, payload } = serviceFnResponse;
34+
const status = typeof type === 'number' ? type : (KeyToHttpStatus as any)[type];
35+
if (status === undefined) {
36+
console.warn('Unknown status code returned');
37+
expressRes.status(500).end();
38+
return;
39+
}
40+
const responseCodec = route.response[status];
41+
if (responseCodec === undefined || !responseCodec.is(payload)) {
42+
console.warn(
43+
"Unable to encode route's return value, did you return the expected type?",
44+
);
45+
expressRes.status(500).end();
46+
return;
47+
}
48+
49+
expressRes.status(status).json(responseCodec.encode(payload)).end();
50+
};

0 commit comments

Comments
 (0)