Skip to content

Commit be21cde

Browse files
nabdelgadirbajtos
andcommitted
feat(rest): add mountExpressRouter
Co-authored-by: Miroslav Bajtoš <[email protected]>
1 parent f83288d commit be21cde

11 files changed

+618
-13
lines changed

docs/site/Routes.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,64 @@ export class MyApplication extends RestApplication {
226226
}
227227
}
228228
```
229+
230+
## Mounting an Express Router
231+
232+
If you have an existing [Express](https://expressjs.com/) application that you
233+
want to use with LoopBack 4, you can mount the Express application on top of a
234+
LoopBack 4 application. This way you can mix and match both frameworks, while
235+
using LoopBack as the host. You can also do the opposite and use Express as the
236+
host by mounting LoopBack 4 REST API on an Express application. See
237+
[Creating an Express Application with LoopBack REST API](express-with-lb4-rest-tutorial.md)
238+
for the tutorial.
239+
240+
Mounting an Express router on a LoopBack 4 application can be done using the
241+
`mountExpressRouter` function provided by both
242+
[`RestApplication`](http://apidocs.loopback.io/@loopback%2fdocs/rest.html#RestApplication)
243+
and
244+
[`RestServer`](http://apidocs.loopback.io/@loopback%2fdocs/rest.html#RestServer).
245+
246+
Example use:
247+
248+
{% include note.html content="
249+
Make sure [express](https://www.npmjs.com/package/express) is installed.
250+
" %}
251+
252+
{% include code-caption.html content="src/express-app.ts" %}
253+
254+
```ts
255+
import {Request, Response} from 'express';
256+
import * as express from 'express';
257+
258+
const legacyApp = express();
259+
260+
// your existing Express routes
261+
legacyApp.get('/pug', function(_req: Request, res: Response) {
262+
res.send('Pug!');
263+
});
264+
265+
export {legacyApp};
266+
```
267+
268+
{% include code-caption.html content="src/application.ts" %}
269+
270+
```ts
271+
import {RestApplication} from '@loopback/rest';
272+
273+
const legacyApp = require('./express-app').legacyApp;
274+
275+
const openApiSpecForLegacyApp: RouterSpec = {
276+
// insert your spec here, your 'paths', 'components', and 'tags' will be used
277+
};
278+
279+
class MyApplication extends RestApplication {
280+
constructor(/* ... */) {
281+
// ...
282+
283+
this.mountExpressRouter('/dogs', legacyApp, openApiSpecForLegacyApp);
284+
}
285+
}
286+
```
287+
288+
Any routes you define in your `legacyApp` will be mounted on top of the `/dogs`
289+
base path, e.g. if you visit the `/dogs/pug` endpoint, you'll see `Pug!`.

docs/site/express-with-lb4-rest-tutorial.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ REST API can be mounted to an Express application and be used as middleware.
1414
This way the user can mix and match features from both frameworks to suit their
1515
needs.
1616

17+
{% include note.html content="
18+
If you want to use LoopBack as the host instead and mount your Express
19+
application on a LoopBack 4 application, see
20+
[Mounting an Express Router](Routes.md#mounting-an-express-router).
21+
" %}
22+
1723
This tutorial assumes familiarity with scaffolding a LoopBack 4 application,
1824
[`Models`](Model.md), [`DataSources`](DataSources.md),
1925
[`Repositories`](Repositories.md), and [`Controllers`](Controllers.md). To see

packages/rest/src/__tests__/integration/rest.application.integration.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
55

66
import {anOperationSpec} from '@loopback/openapi-spec-builder';
77
import {Client, createRestAppClient, expect} from '@loopback/testlab';
8+
import * as express from 'express';
9+
import {Request, Response} from 'express';
810
import * as fs from 'fs';
911
import * as path from 'path';
10-
import {RestApplication, RestServer, RestServerConfig, get} from '../..';
12+
import {
13+
get,
14+
RestApplication,
15+
RestServer,
16+
RestServerConfig,
17+
RouterSpec,
18+
} from '../..';
1119

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

@@ -163,6 +171,103 @@ describe('RestApplication (integration)', () => {
163171
await client.get(response.header.location).expect(200, 'Hi');
164172
});
165173

174+
context('mounting an Express router on a LoopBack application', async () => {
175+
beforeEach('set up RestApplication', async () => {
176+
givenApplication();
177+
await restApp.start();
178+
client = createRestAppClient(restApp);
179+
});
180+
181+
it('gives precedence to an external route over a static route', async () => {
182+
const router = express.Router();
183+
router.get('/', function(_req: Request, res: Response) {
184+
res.send('External dog');
185+
});
186+
187+
restApp.static('/dogs', ASSETS);
188+
restApp.mountExpressRouter('/dogs', router);
189+
190+
await client.get('/dogs/').expect(200, 'External dog');
191+
});
192+
193+
it('mounts an express Router without spec', async () => {
194+
const router = express.Router();
195+
router.get('/poodle/', function(_req: Request, res: Response) {
196+
res.send('Poodle!');
197+
});
198+
router.get('/pug', function(_req: Request, res: Response) {
199+
res.send('Pug!');
200+
});
201+
restApp.mountExpressRouter('/dogs', router);
202+
203+
await client.get('/dogs/poodle/').expect(200, 'Poodle!');
204+
await client.get('/dogs/pug').expect(200, 'Pug!');
205+
});
206+
207+
it('mounts an express Router with spec', async () => {
208+
const router = express.Router();
209+
function greetDogs(_req: Request, res: Response) {
210+
res.send('Hello dogs!');
211+
}
212+
213+
const spec: RouterSpec = {
214+
paths: {
215+
'/hello': {
216+
get: {
217+
responses: {
218+
'200': {
219+
description: 'greet the dogs',
220+
content: {
221+
'text/plain': {
222+
schema: {type: 'string'},
223+
},
224+
},
225+
},
226+
},
227+
},
228+
},
229+
},
230+
};
231+
router.get('/hello', greetDogs);
232+
restApp.mountExpressRouter('/dogs', router, spec);
233+
await client.get('/dogs/hello').expect(200, 'Hello dogs!');
234+
235+
const openApiSpec = restApp.restServer.getApiSpec();
236+
expect(openApiSpec.paths).to.deepEqual({
237+
'/dogs/hello': {
238+
get: {
239+
responses: {
240+
'200': {
241+
description: 'greet the dogs',
242+
content: {'text/plain': {schema: {type: 'string'}}},
243+
},
244+
},
245+
},
246+
},
247+
});
248+
});
249+
250+
it('mounts more than one express Router', async () => {
251+
const router = express.Router();
252+
router.get('/poodle', function(_req: Request, res: Response) {
253+
res.send('Poodle!');
254+
});
255+
256+
restApp.mountExpressRouter('/dogs', router);
257+
258+
const secondRouter = express.Router();
259+
260+
secondRouter.get('/persian', function(_req: Request, res: Response) {
261+
res.send('Persian cat.');
262+
});
263+
264+
restApp.mountExpressRouter('/cats', secondRouter);
265+
266+
await client.get('/dogs/poodle').expect(200, 'Poodle!');
267+
await client.get('/cats/persian').expect(200, 'Persian cat.');
268+
});
269+
});
270+
166271
function givenApplication(options?: {rest: RestServerConfig}) {
167272
options = options || {rest: {port: 0, host: '127.0.0.1'}};
168273
restApp = new RestApplication(options);
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright IBM Corp. 2019. All Rights Reserved.
2+
// Node module: @loopback/rest
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {expect} from '@loopback/testlab';
7+
import {assignRouterSpec, RouterSpec} from '../../../';
8+
9+
describe('assignRouterSpec', () => {
10+
it('duplicates the additions spec if the target spec is empty', async () => {
11+
const target: RouterSpec = {paths: {}};
12+
const additions: RouterSpec = {
13+
paths: {
14+
'/': {
15+
get: {
16+
responses: {
17+
'200': {
18+
description: 'greeting',
19+
content: {
20+
'application/json': {
21+
schema: {type: 'string'},
22+
},
23+
},
24+
},
25+
},
26+
},
27+
},
28+
},
29+
components: {
30+
schemas: {
31+
Greeting: {
32+
type: 'object',
33+
properties: {
34+
message: {
35+
type: 'string',
36+
},
37+
},
38+
},
39+
},
40+
},
41+
tags: [{name: 'greeting', description: 'greetings'}],
42+
};
43+
44+
assignRouterSpec(target, additions);
45+
expect(target).to.eql(additions);
46+
});
47+
48+
it('does not assign components without schema', async () => {
49+
const target: RouterSpec = {
50+
paths: {},
51+
components: {},
52+
};
53+
54+
const additions: RouterSpec = {
55+
paths: {},
56+
components: {
57+
parameters: {
58+
addParam: {
59+
name: 'add',
60+
in: 'query',
61+
description: 'number of items to add',
62+
required: true,
63+
schema: {
64+
type: 'integer',
65+
format: 'int32',
66+
},
67+
},
68+
},
69+
responses: {
70+
Hello: {
71+
description: 'Hello.',
72+
},
73+
},
74+
},
75+
};
76+
77+
assignRouterSpec(target, additions);
78+
expect(target.components).to.be.empty();
79+
});
80+
81+
it('uses the route registered first', async () => {
82+
const originalPath = {
83+
'/': {
84+
get: {
85+
responses: {
86+
'200': {
87+
description: 'greeting',
88+
content: {
89+
'application/json': {
90+
schema: {type: 'string'},
91+
},
92+
},
93+
},
94+
},
95+
},
96+
},
97+
};
98+
99+
const target: RouterSpec = {paths: originalPath};
100+
101+
const additions: RouterSpec = {
102+
paths: {
103+
'/': {
104+
get: {
105+
responses: {
106+
'200': {
107+
description: 'additional greeting',
108+
content: {
109+
'application/json': {
110+
schema: {type: 'string'},
111+
},
112+
},
113+
},
114+
'404': {
115+
description: 'Error: no greeting',
116+
content: {
117+
'application/json': {
118+
schema: {type: 'string'},
119+
},
120+
},
121+
},
122+
},
123+
},
124+
},
125+
},
126+
};
127+
128+
assignRouterSpec(target, additions);
129+
expect(target.paths).to.eql(originalPath);
130+
});
131+
132+
it('does not duplicate tags from the additional spec', async () => {
133+
const target: RouterSpec = {
134+
paths: {},
135+
tags: [{name: 'greeting', description: 'greetings'}],
136+
};
137+
const additions: RouterSpec = {
138+
paths: {},
139+
tags: [
140+
{name: 'greeting', description: 'additional greetings'},
141+
{name: 'salutation', description: 'salutations!'},
142+
],
143+
};
144+
145+
assignRouterSpec(target, additions);
146+
expect(target.tags).to.containDeep([
147+
{name: 'greeting', description: 'greetings'},
148+
{name: 'salutation', description: 'salutations!'},
149+
]);
150+
});
151+
});

0 commit comments

Comments
 (0)