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 f83288d commit be21cde
Show file tree
Hide file tree
Showing 11 changed files with 618 additions and 13 deletions.
61 changes: 61 additions & 0 deletions docs/site/Routes.md
Expand Up @@ -226,3 +226,64 @@ 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. You can also do the opposite and use Express as the
host by mounting LoopBack 4 REST API on an Express application. See
[Creating an Express Application with LoopBack REST API](express-with-lb4-rest-tutorial.md)
for the tutorial.

Mounting an Express router on a LoopBack 4 application can be done using the
`mountExpressRouter` function provided by both
[`RestApplication`](http://apidocs.loopback.io/@loopback%2fdocs/rest.html#RestApplication)
and
[`RestServer`](http://apidocs.loopback.io/@loopback%2fdocs/rest.html#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!');
});

export {legacyApp};
```

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

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

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

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

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

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

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!`.
6 changes: 6 additions & 0 deletions docs/site/express-with-lb4-rest-tutorial.md
Expand Up @@ -14,6 +14,12 @@ REST API can be mounted to an Express application and be used as middleware.
This way the user can mix and match features from both frameworks to suit their
needs.

{% include note.html content="
If you want to use LoopBack as the host instead and mount your Express
application on a LoopBack 4 application, see
[Mounting an Express Router](Routes.md#mounting-an-express-router).
" %}

This tutorial assumes familiarity with scaffolding a LoopBack 4 application,
[`Models`](Model.md), [`DataSources`](DataSources.md),
[`Repositories`](Repositories.md), and [`Controllers`](Controllers.md). To see
Expand Down
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,103 @@ 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 RestApplication', 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: {
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!');

const openApiSpec = restApp.restServer.getApiSpec();
expect(openApiSpec.paths).to.deepEqual({
'/dogs/hello': {
get: {
responses: {
'200': {
description: 'greet the dogs',
content: {'text/plain': {schema: {type: 'string'}}},
},
},
},
},
});
});

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
151 changes: 151 additions & 0 deletions packages/rest/src/__tests__/unit/router/assign-router-spec.unit.ts
@@ -0,0 +1,151 @@
// 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 {expect} from '@loopback/testlab';
import {assignRouterSpec, RouterSpec} from '../../../';

describe('assignRouterSpec', () => {
it('duplicates the additions spec if the target spec is empty', async () => {
const target: RouterSpec = {paths: {}};
const additions: RouterSpec = {
paths: {
'/': {
get: {
responses: {
'200': {
description: 'greeting',
content: {
'application/json': {
schema: {type: 'string'},
},
},
},
},
},
},
},
components: {
schemas: {
Greeting: {
type: 'object',
properties: {
message: {
type: 'string',
},
},
},
},
},
tags: [{name: 'greeting', description: 'greetings'}],
};

assignRouterSpec(target, additions);
expect(target).to.eql(additions);
});

it('does not assign components without schema', async () => {
const target: RouterSpec = {
paths: {},
components: {},
};

const additions: RouterSpec = {
paths: {},
components: {
parameters: {
addParam: {
name: 'add',
in: 'query',
description: 'number of items to add',
required: true,
schema: {
type: 'integer',
format: 'int32',
},
},
},
responses: {
Hello: {
description: 'Hello.',
},
},
},
};

assignRouterSpec(target, additions);
expect(target.components).to.be.empty();
});

it('uses the route registered first', async () => {
const originalPath = {
'/': {
get: {
responses: {
'200': {
description: 'greeting',
content: {
'application/json': {
schema: {type: 'string'},
},
},
},
},
},
},
};

const target: RouterSpec = {paths: originalPath};

const additions: RouterSpec = {
paths: {
'/': {
get: {
responses: {
'200': {
description: 'additional greeting',
content: {
'application/json': {
schema: {type: 'string'},
},
},
},
'404': {
description: 'Error: no greeting',
content: {
'application/json': {
schema: {type: 'string'},
},
},
},
},
},
},
},
};

assignRouterSpec(target, additions);
expect(target.paths).to.eql(originalPath);
});

it('does not duplicate tags from the additional spec', async () => {
const target: RouterSpec = {
paths: {},
tags: [{name: 'greeting', description: 'greetings'}],
};
const additions: RouterSpec = {
paths: {},
tags: [
{name: 'greeting', description: 'additional greetings'},
{name: 'salutation', description: 'salutations!'},
],
};

assignRouterSpec(target, additions);
expect(target.tags).to.containDeep([
{name: 'greeting', description: 'greetings'},
{name: 'salutation', description: 'salutations!'},
]);
});
});

0 comments on commit be21cde

Please sign in to comment.