Skip to content

Commit 18f04d1

Browse files
feat(express): api-ts setup
2 parents 80839d5 + 8842c5e commit 18f04d1

File tree

9 files changed

+261
-12
lines changed

9 files changed

+261
-12
lines changed

modules/express/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
"morgan": "^1.9.1",
5454
"proxy-agent": "6.4.0",
5555
"proxyquire": "^2.1.3",
56-
"superagent": "^9.0.1"
56+
"superagent": "^9.0.1",
57+
"@api-ts/io-ts-http": "^3.2.1",
58+
"@api-ts/typed-express-router": "^1.1.13"
5759
},
5860
"devDependencies": {
5961
"@bitgo/public-types": "5.1.0",

modules/express/src/clientRoutes.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,25 @@ import { handleUpdateLightningWalletCoinSpecific } from './lightning/lightningWa
5959
import { ProxyAgent } from 'proxy-agent';
6060
import { isLightningCoinName } from '@bitgo/abstract-lightning';
6161
import { handleLightningWithdraw } from './lightning/lightningWithdrawRoutes';
62+
import createExpressRouter from './typedRoutes';
63+
import { ExpressApiRouteRequest } from './typedRoutes/api';
64+
import { TypedRequestHandler, WrappedRequest, WrappedResponse } from '@api-ts/typed-express-router';
6265

6366
const { version } = require('bitgo/package.json');
6467
const pjson = require('../package.json');
6568
const debug = debugLib('bitgo:express');
6669

6770
const BITGOEXPRESS_USER_AGENT = `BitGoExpress/${pjson.version} BitGoJS/${version}`;
6871

69-
function handlePing(req: express.Request, res: express.Response, next: express.NextFunction) {
72+
function handlePing(
73+
req: ExpressApiRouteRequest<'express.ping', 'get'>,
74+
res: express.Response,
75+
next: express.NextFunction
76+
) {
7077
return req.bitgo.ping();
7178
}
7279

73-
function handlePingExpress(req: express.Request) {
80+
function handlePingExpress(req: ExpressApiRouteRequest<'express.pingExpress', 'get'>) {
7481
return {
7582
status: 'express server is ok!',
7683
};
@@ -1358,6 +1365,23 @@ export function promiseWrapper(promiseRequestHandler: RequestHandler) {
13581365
};
13591366
}
13601367

1368+
export function typedPromiseWrapper(promiseRequestHandler: TypedRequestHandler) {
1369+
return async function (req: WrappedRequest, res: WrappedResponse, next: express.NextFunction) {
1370+
debug(`handle: ${req.method} ${req.originalUrl}`);
1371+
try {
1372+
const result = await promiseRequestHandler(req, res, next);
1373+
if (typeof result === 'object' && result !== null && 'body' in result && 'status' in result) {
1374+
const { status, body } = result as { status: number; body: unknown };
1375+
res.status(status).send(body);
1376+
} else {
1377+
res.status(200).send(result);
1378+
}
1379+
} catch (e) {
1380+
handleRequestHandlerError(res, e);
1381+
}
1382+
};
1383+
}
1384+
13611385
export function createCustomSigningFunction(externalSignerUrl: string): CustomSigningFunction {
13621386
return async function (params): Promise<SignedTransaction> {
13631387
const { body: signedTx } = await retryPromise(
@@ -1530,8 +1554,11 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
15301554
// ping
15311555
// /api/v[12]/pingexpress is the only exception to the rule above, as it explicitly checks the health of the
15321556
// express server without running into rate limiting with the BitGo server.
1533-
app.get('/api/v[12]/ping', prepareBitGo(config), promiseWrapper(handlePing));
1534-
app.get('/api/v[12]/pingexpress', promiseWrapper(handlePingExpress));
1557+
const router = createExpressRouter();
1558+
app.use(router);
1559+
1560+
router.get('express.ping', [prepareBitGo(config), typedPromiseWrapper(handlePing)]);
1561+
router.get('express.pingExpress', [typedPromiseWrapper(handlePingExpress)]);
15351562

15361563
// auth
15371564
app.post('/api/v[12]/user/login', parseBody, prepareBitGo(config), promiseWrapper(handleLogin));
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Ping
7+
*
8+
* @operationId express.ping
9+
*/
10+
export const GetPing = httpRoute({
11+
path: '/api/v[12]/ping',
12+
method: 'GET',
13+
request: httpRequest({}),
14+
response: {
15+
200: t.type({}),
16+
404: BitgoExpressError,
17+
},
18+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Ping Express
7+
*
8+
* @operationId express.pingExpress
9+
*/
10+
export const GetPingExpress = httpRoute({
11+
path: '/api/v[12]/pingexpress',
12+
method: 'GET',
13+
request: httpRequest({}),
14+
response: {
15+
200: t.type({ status: t.string }),
16+
404: BitgoExpressError,
17+
},
18+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as t from 'io-ts';
2+
import { apiSpec } from '@api-ts/io-ts-http';
3+
import * as express from 'express';
4+
5+
import { GetPing } from './common/ping';
6+
import { GetPingExpress } from './common/pingExpress';
7+
8+
export const ExpressApi = apiSpec({
9+
'express.ping': {
10+
get: GetPing,
11+
},
12+
'express.pingExpress': {
13+
get: GetPingExpress,
14+
},
15+
});
16+
17+
export type ExpressApi = typeof ExpressApi;
18+
19+
type ExtractDecoded<T> = T extends t.Type<any, infer O, any> ? O : never;
20+
export type ExpressApiRouteRequest<
21+
ApiName extends keyof ExpressApi,
22+
Method extends keyof ExpressApi[ApiName]
23+
> = ExpressApi[ApiName][Method] extends { request: infer R } ? express.Request & { decoded: ExtractDecoded<R> } : never;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createRouter, WrappedRouter } from '@api-ts/typed-express-router';
2+
3+
import { ExpressApi } from './api';
4+
5+
export default function (): WrappedRouter<ExpressApi> {
6+
const router: WrappedRouter<ExpressApi> = createRouter(ExpressApi);
7+
return router;
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as t from 'io-ts';
2+
3+
export const BitgoExpressError = t.type({
4+
message: t.string,
5+
name: t.string,
6+
bitgoJsVersion: t.string,
7+
bitgoExpressVersion: t.string,
8+
});

modules/express/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"compilerOptions": {
44
"outDir": "./dist",
55
"rootDir": ".",
6+
"allowSyntheticDefaultImports": true,
67
"typeRoots": ["./types", "../../types", "./node_modules/@types", "../../node_modules/@types"]
78
},
89
"include": ["src/**/*", "test/**/*", "package.json"],

0 commit comments

Comments
 (0)