Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(rest): add mountExpressRouter #2643

Merged
merged 1 commit into from
Mar 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/site/Routes.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
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,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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we silently replace the static route in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it's looking for the route, it first checks if there's an external route, then if it can't find it then it uses the static one (see here), so I believe the static one still exists, but is overshadowed by the external one.

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

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

it('mounts an express Router without spec', async () => {
const router = express.Router();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the default generated(?) spec look like here if one is not provided? Can we add an assertion for it?

Copy link
Contributor Author

@nabdelgadir nabdelgadir Mar 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec is optional, so if it isn't provided then it won't get generated or appear in openapi.json.

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
Original file line number Diff line number Diff line change
@@ -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!'},
]);
});
});
Loading