Skip to content

Commit

Permalink
feat(rest): add mountExpressRouter
Browse files Browse the repository at this point in the history
Co-authored-by: Miroslav Bajtoš <mbajtoss@gmail.com>
  • Loading branch information
nabdelgadir and bajtos committed Mar 29, 2019
1 parent 124c078 commit 4decf7b
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 13 deletions.
54 changes: 54 additions & 0 deletions docs/site/Routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,57 @@ export class MyApplication extends RestApplication {
}
}
```

## Mounting an Express Router

If you have an existing [Express](https://expressjs.com/) application that you
want to use with LoopBack 4, you can mount the Express application on top of a
LoopBack 4 application. This way you can mix and match both frameworks, while
using LoopBack as the host. Mounting an Express router on a LoopBack 4
application can be done using the `mountExpressRouter` function provided by both
`RestApplication` and `RestServer`.

Example use:

{% include note.html content="
Make sure [express](https://www.npmjs.com/package/express) is installed.
" %}

{% include code-caption.html content="src/express-app.ts" %}

```ts
import {Request, Response} from 'express';
import * as express from 'express';

const legacyApp = express();

// your existing Express routes
legacyApp.get('/pug', function(_req: Request, res: Response) {
res.send('Pug!');
});

module.exports = legacyApp;
```

{% include code-caption.html content="src/application.ts" %}

```ts
import {RestApplication} from '@loopback/rest';

const legacyApp = require('./express-app');

const legacySpec: RouterSpec = {
// insert your spec here, your 'paths', 'components', and 'tags' will be used
};

class MyApplication extends RestApplication {
constructor(/* ... */) {
// ...

this.mountExpressRouter('/dogs', legacyApp, legacySpec);
}
}
```

Any routes you define in your `legacyApp` will be mounted on top of the `/dogs`
base path, e.g. if you visit the `/dogs/pug` endpoint, you'll see `Pug!`.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@

import {anOperationSpec} from '@loopback/openapi-spec-builder';
import {Client, createRestAppClient, expect} from '@loopback/testlab';
import * as express from 'express';
import {Request, Response} from 'express';
import * as fs from 'fs';
import * as path from 'path';
import {RestApplication, RestServer, RestServerConfig, get} from '../..';
import {
get,
RestApplication,
RestServer,
RestServerConfig,
RouterSpec,
} from '../..';

const ASSETS = path.resolve(__dirname, '../../../fixtures/assets');

Expand Down Expand Up @@ -163,6 +171,90 @@ describe('RestApplication (integration)', () => {
await client.get(response.header.location).expect(200, 'Hi');
});

context('mounting an Express router on a LoopBack application', async () => {
beforeEach('set up rest application', async () => {
givenApplication();
await restApp.start();
client = createRestAppClient(restApp);
});

it('gives precedence to an external route over a static route', async () => {
const router = express.Router();
router.get('/', function(_req: Request, res: Response) {
res.send('External dog');
});

restApp.static('/dogs', ASSETS);
restApp.mountExpressRouter('/dogs', router);

await client.get('/dogs/').expect(200, 'External dog');
});

it('mounts an express Router without spec', async () => {
const router = express.Router();
router.get('/poodle/', function(_req: Request, res: Response) {
res.send('Poodle!');
});
router.get('/pug', function(_req: Request, res: Response) {
res.send('Pug!');
});
restApp.mountExpressRouter('/dogs', router);

await client.get('/dogs/poodle/').expect(200, 'Poodle!');
await client.get('/dogs/pug').expect(200, 'Pug!');
});

it('mounts an express Router with spec', async () => {
const router = express.Router();
function greetDogs(_req: Request, res: Response) {
res.send('Hello dogs!');
}

const spec: RouterSpec = {
paths: {
'/hello': {
get: {
'x-operation': greetDogs,
responses: {
'200': {
description: 'greet the dogs',
content: {
'text/plain': {
schema: {type: 'string'},
},
},
},
},
},
},
},
};
router.get('/hello', greetDogs);
restApp.mountExpressRouter('/dogs', router, spec);
await client.get('/dogs/hello').expect(200, 'Hello dogs!');
});

it('mounts more than one express Router', async () => {
const router = express.Router();
router.get('/poodle', function(_req: Request, res: Response) {
res.send('Poodle!');
});

restApp.mountExpressRouter('/dogs', router);

const secondRouter = express.Router();

secondRouter.get('/persian', function(_req: Request, res: Response) {
res.send('Persian cat.');
});

restApp.mountExpressRouter('/cats', secondRouter);

await client.get('/dogs/poodle').expect(200, 'Poodle!');
await client.get('/cats/persian').expect(200, 'Persian cat.');
});
});

function givenApplication(options?: {rest: RestServerConfig}) {
options = options || {rest: {port: 0, host: '127.0.0.1'}};
restApp = new RestApplication(options);
Expand Down
20 changes: 20 additions & 0 deletions packages/rest/src/__tests__/unit/router/assign-router-spec.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {RouterSpec} from '../../../router';

describe('assignRouterSpec', () => {
// tslint:disable:no-unused - will remove once implemented
const target: RouterSpec = {paths: {}, components: {}, tags: []};
const additions: RouterSpec = {paths: {}, components: {}, tags: []};

it('duplicates the additions spec if the target spec is empty', async () => {});

it('does not assign components without schema', async () => {});

it('uses the route registered first', async () => {});

it('does not duplicate tags from the additional spec', async () => {});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

describe('rebaseOpenApiSpec', () => {
it('does not modify the OpenApiSpec if it does not have paths', async () => {});

it('does not modify the OpenApiSpec if basePath is empty or `/`', async () => {});

it('rebases OpenApiSpec if there is a basePath', async () => {});

it('does not modify the original OpenApiSpec', async () => {});
});
31 changes: 28 additions & 3 deletions packages/rest/src/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Binding, Constructor, BindingAddress} from '@loopback/context';
import {Binding, BindingAddress, Constructor} from '@loopback/context';
import {Application, ApplicationConfig, Server} from '@loopback/core';
import {OpenApiSpec, OperationObject} from '@loopback/openapi-v3-types';
import {PathParams} from 'express-serve-static-core';
import {ServeStaticOptions} from 'serve-static';
import {format} from 'util';
import {BodyParser} from './body-parsers';
import {RestBindings} from './keys';
import {RestComponent} from './rest.component';
import {HttpRequestListener, HttpServerLike, RestServer} from './rest.server';
import {ControllerClass, ControllerFactory, RouteEntry} from './router';
import {
ControllerClass,
ControllerFactory,
ExpressRequestHandler,
RouteEntry,
} from './router';
import {RouterSpec} from './router/router-spec';
import {SequenceFunction, SequenceHandler} from './sequence';
import {BodyParser} from './body-parsers';

export const ERR_NO_MULTI_SERVER = format(
'RestApplication does not support multiple servers!',
Expand Down Expand Up @@ -264,4 +270,23 @@ export class RestApplication extends Application implements HttpServerLike {
api(spec: OpenApiSpec): Binding {
return this.bind(RestBindings.API_SPEC).to(spec);
}

/**
* Mount an Express router to expose additional REST endpoints handled
* via legacy Express-based stack.
*
* @param basePath Path where to mount the router at, e.g. `/` or `/api`.
* @param router The Express router to handle the requests.
* @param spec A partial OpenAPI spec describing endpoints provided by the
* router. LoopBack will prepend `basePath` to all endpoints automatically.
* This argument is optional. You can leave it out if you don't want to
* document the routes.
*/
mountExpressRouter(
basePath: string,
router: ExpressRequestHandler,
spec?: RouterSpec,
): void {
this.restServer.mountExpressRouter(basePath, router, spec);
}
}
24 changes: 24 additions & 0 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ import {
ControllerInstance,
ControllerRoute,
createControllerFactoryForBinding,
ExpressRequestHandler,
ExternalExpressRoutes,
RedirectRoute,
RestRouterOptions,
Route,
RouteEntry,
RouterSpec,
RoutingTable,
} from './router';
import {assignRouterSpec} from './router/router-spec';
import {DefaultSequence, SequenceFunction, SequenceHandler} from './sequence';
import {
FindRoute,
Expand Down Expand Up @@ -681,6 +684,8 @@ export class RestServer extends Context implements Server, HttpServerLike {
spec.components = spec.components || {};
spec.components.schemas = cloneDeep(defs);
}

assignRouterSpec(spec, this._externalRoutes.routerSpec);
return spec;
}

Expand Down Expand Up @@ -827,6 +832,25 @@ export class RestServer extends Context implements Server, HttpServerLike {
throw err;
});
}

/**
* Mount an Express router to expose additional REST endpoints handled
* via legacy Express-based stack.
*
* @param basePath Path where to mount the router at, e.g. `/` or `/api`.
* @param router The Express router to handle the requests.
* @param spec A partial OpenAPI spec describing endpoints provided by the
* router. LoopBack will prepend `basePath` to all endpoints automatically.
* This argument is optional. You can leave it out if you don't want to
* document the routes.
*/
mountExpressRouter(
basePath: string,
router: ExpressRequestHandler,
spec?: RouterSpec,
): void {
this._externalRoutes.mountRouter(basePath, router, spec);
}
}

/**
Expand Down
Loading

0 comments on commit 4decf7b

Please # to comment.