UNPKG

21.4 kBMarkdownView Raw
1# Contents
2
3- [Defining Routes in QEWD-Up](#defining-routes-in-qewd-up)
4- [Route Definitions](#route-definitions)
5 - [Monolithic Applications](#monolithic-applications)
6 - [MicroService Applications](#microservice-applications)
7 - [APIs Handled on a Single MicroService](#apis-handled-on-a-single-microservice)
8 - [APIs Handled on Multiple MicroServices](#apis-handled-on-multiple-microservices)
9 - [Internal MicroService APIs](#internal-microservice-apis)
10- [Over-rides](#over-rides)
11 - [The beforeHandler Hook in Monolithic Applications](#the-beforehandler-hook-in-monolithic-applications)
12 - [Authenticated JWTs in MicroService Applications](#authenticated-jwts-in-microservice-applications)
13- [Customising Invalid Route Error Responses](#customising-invalid-route-error-responses)
14- [Variable API Paths](#variable-api-paths)
15- [Variable MicroService Destinations](#variable-microservice-destinations)
16- [Federated Data Using Group Destinations](#federated-data-using-group-destinations)
17- [Dynamically Routed APIs](#dynamically-routed-apis)
18
19# Defining Routes in QEWD-Up
20
21Routes are defined in the */configuration/routes.json* file as an array of route objects.
22
23These Route definitions are used:
24
25- by each QEWD-Up instance when you first start it up
26- by each QEWD Worker process when it is first started by its Master process
27
28If you modify the Route definitions in any way, you must restart:
29
30- the QEWD-Up instance if you are running a monolithic application;
31- the Orchestrator QEWD-Up instance and any QEWD-Up MicroService instances that handle the modified routes.
32
33# Route Definitions
34
35## Monolithic Applications
36
37For monolithic applications, most routes can be described by an object containing three properties:
38
39- **uri**: the API path
40- **method**: the HTTP method (GET by default)
41- **handler**: the name of your handler module that will perform the processing of the request
42
43For example:
44
45 [
46 {
47 "uri": "/api/orchestrator/info",
48 "method": "GET",
49 "handler": "getInfo"
50 }
51 ]
52
53## MicroService Applications
54
55For Micro-Service applications, most route definitions require the same three properties:
56
57- **uri**: the API path
58- **method**: the HTTP method (GET by default)
59- **handler**: the name of your handler module that will perform the processing of the request
60
61
62If the API is to be hancled on the Orchestrator, these three properties are sufficient.
63
64However, if the API is to be handled on a MicroService, you must also specify the name of the MicroService(s) on which the API's *handler* module will run.
65
66### APIs Handled on a Single MicroService
67
68If the API is to be handled on just one MicroService, you add the property:
69
70- **on_microservice**: the name of the MicroService on which the *handler* module will run
71
72MicroService names (and their physical configurations) are defined in the */configuration/config.json* file.
73
74For example:
75
76 [
77 {
78 "uri": "/api/info/info",
79 "method": "GET",
80 "handler": "getInfo",
81 "on_microservice": "info_service"
82 }
83 ]
84
85
86### APIs Handled on Multiple MicroServices
87
88If the API is to be handled on more than one MicroService, you add the property:
89
90- **on_microservices**: an array of MicroService names on which the *handler* module will run
91
92MicroService names (and their physical configurations) are defined in the */configuration/config.json* file.
93
94You only need to define the *handler* module logic once within the relevant API folder for just one of the MicroServices and add the route property
95
96- **handler_source**: the name of the MicroService in which you've defined the *handler* module logic.
97
98For example:
99
100 [
101 {
102 "uri": "/api/:destination/getStock",
103 "method": "GET",
104 "handler": "getStock",
105 "on_microservices": [
106 "info_service",
107 "login_service"
108 ],
109 "handler_source": "info_service"
110 }
111 ]
112
113
114## *Internal* MicroService APIs
115
116You can use the [*onMSResponse*](https://github.com/robtweed/qewd/blob/master/up/docs/Life_Cycle_Events.md#onmsresponse) Event Hook to chain MicroService APIs - ie a MicroService can forward one or more API requests that will be handled on other MicroServices. You may not want these APIs to be publicly accessible via the Orchestrator, but instead be *internally* accessible only.
117
118QEWD-Up therefore allows you to define routes that are only accessible from one or more MicroServices. Simply add the *route* property:
119
120- **from_microservices**: array of MicroService names that are allowed to invoke requests for the API.
121
122For example:
123
124
125 {
126 "uri": "/api/info/demographics",
127 "method": "GET",
128 "handler": "getDemographics",
129 "on_microservice": "info_service",
130 "from_microservices": [
131 "login_service"
132 ]
133 }
134
135In the above example, the */api/info/demographics* API (which runs on the *info_service* MicroService) can only be accessed from the *login_service* MicroService.
136
137
138
139# Over-rides
140
141You can over-ride a number of otherwise automatically-applied steps that are handled by QEWD.
142
143## The *beforeHandler* Hook in Monolithic Applications
144
145In Monolithic applications, you can apply a *beforeHandler* hook that is invoked in the QEWD Worker process just before your *handler* module is invoked. A *beforeHandler* hook is a handy way of applying tests to all of your APIs, for example to ensure that the user who is sending the API request has been authenticated.
146
147In the case of a *beforeHandler* hook that tests for user authentication (eg checking the value of the *Authorization* header for a valid QEWD Session token), you'll probably **NOT** want to apply its logic to the API with which the user first authenticates themselves. Hence you'll need to over-ride the *beforeHandler* hook for this API.
148
149To over-ride the *beforeHandler* hook, simply add the *route* property:
150
151- **applyBeforeHandler**: *false*
152
153For example:
154
155
156 [
157 {
158 "uri": "/api/login",
159 "method": "POST",
160 "handler": "login",
161 "applyBeforeHandler": false
162 },
163 {
164 "uri": "/api/info",
165 "method": "GET",
166 "handler": "getInfo"
167 }
168 ]
169
170
171So in the example above, your authentication tests in the *beforeHandler* hook will be applied to the */api/info* route, but **not** to the */api/login* route.
172
173
174## Authenticated JWTs in MicroService Applications
175
176In MicroService applications, QEWD-Up will check incoming requests on the Orchestrator to ensure that they include a valid, un-expired JWT in the *Authorization* header, **AND** that the *authenticated* property within the JWT is set to *true*.
177
178Of course, you'll need to over-ride these tests in at least one of your APIs: the one(s) used to authenticate the user. You over-ride this JWT test by adding the *route* property:
179
180- **authenticate**: *false*
181
182For example:
183
184 [
185 {
186 "uri": "/api/login",
187 "method": "POST",
188 "handler": "login",
189 "on_microservice": "login_service",
190 "authenticate": false
191 },
192 {
193 "uri": "/api/info/info",
194 "method": "GET",
195 "handler": "getInfo",
196 "on_microservice": "info_service"
197 }
198 ]
199
200In the example above, the JWT is **not** tested for incoming */api/login* requests, meaning that such requests do not need to include an *Authorization* header at all. However, a valid, authenticated JWT must be present in the */api/info/info* request's *Authorization* header.
201
202The handler for the *login* API on the *login_service* MicroService will set the JWT's *authenticated* property if the user's login credentials are satisfactory. For example:
203
204 module.exports = function(args, finished) {
205 var username = args.req.body.username;
206 var password = args.req.body.password;
207 var jwt = args.session;
208 if (username === 'rob' && password === 'secret') {
209 jwt.authenticated = true; // *** Sets the JWT's authenticated property ***
210 jwt.timeout = 1200;
211 finished({
212 ok: true
213 });
214 }
215 else {
216 finished({error: 'Invalid login'});
217 }
218 };
219
220In the example above, if valid username/password credentials are added to the *POST* request's *body* payload, then the response willl include an authenticated JWT in the response, eg:
221
222 {
223 "ok":true,
224 "token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDY1MDk5MjQsImlhdCI6MTU0NjUwODcyNCwiaXNzIjoicWV3ZC5qd3QiLCJhcHBsaWNhdGlvbiI6ImxvZ2luX3NlcnZpY2UiLCJ0aW1lb3V0IjoxMjAwLCJxZXdkIjoiNzA0ZDg1YTJhZGM2OWI4ODI0NjJiN2RiMThlNGE1Y2M4MjEwMjAzMTJhY2RmNDkxNmZkM2ZhY2IwMWFlMWEyODUzN2Y4ZDQ2MWYzMjQyNThjYmIyNjhkMTQ4ODgzNjY2MjRmYmU1YmFkNTUzYjMwNmI0ODkwMjg0MDRkZjNjZjZlYzkzMjMyYThiNDgwYmQzZmY4OGZlMWZkNTIzYjUwZDk3OGFiYjViNjdlZTZkYWRlNzlkYzI4Y2I0MWQzOTQ4In0.wIhiIJzttrmFNcQUn04FGOO29tPi2yz3grXs63Yiivw"
225 }
226
227**Note**: if you decode this JWT, don't be surprised that you don't actually see a visible *authenticated* property - QEWD encrypts it and holds it within its *qewd* Claim, eg:
228
229
230 {
231 "exp": 1546509924,
232 "iat": 1546508724,
233 "iss": "qewd.jwt",
234 "application": "login_service",
235 "timeout": 1200,
236 "qewd": "704d85a2adc69b882462b7db18e4a5cc821020312acdf4916fd3facb01ae1a28537f8d461f324258cbb268d14888366624fbe5bad553b306b489028404df3cf6ec93232a8b480bd3ff88fe1fd523b50d978abb5b67ee6dade79dc28cb41d3948"
237 }
238
239However, the *authenticated* property value is automatically decrypted by QEWD and made available to your API handler modules (along with any other encrypted values within the *qewd* Claim).
240
241
242In the example above, if invalid user credentials had been supplied, then the API response would be an Error response without a JWT.
243
244
245# Customising Invalid Route Error Responses
246
247QEWD automatically returns a default Error response if requests are submitted for API routes that are not defined in the *routes.json* file. By default an HTTP response with a Status of *400* will be returned, and it will include a JSON response payload as follows:
248
249 {
250 "error":"No handler defined for api messages of type {{xxx}}"
251 }
252
253The *type* in the error response will actually correspond to the 2nd part of the API path.
254
255You can take control over the error responses returned for invalid API requests. Simply add an **else** object to the end of your *routes* array, defining the HTTP status code and error text you want to return.
256
257For example:
258
259 [
260 {
261 "uri": "/api/login",
262 "method": "POST",
263 "handler": "login",
264 "on_microservice": "login_service",
265 "authenticate": false
266 },
267 {
268 "uri": "/api/info/info",
269 "method": "GET",
270 "handler": "getInfo",
271 "on_microservice": "info_service"
272 },
273 {
274 "else": {
275 "statusCode": 404,
276 "text": "Not Found"
277 }
278 }
279 ]
280
281Now, any API request other than *POST /api/login* or *GET /api/info/info* will return a *404* error and a JSON error response payload:
282
283 {
284 "error": "Not Found"
285 }
286
287
288
289# Variable API Paths
290
291Your API route paths can contain one or more variable components. These are simply specified with a preceding colon. For example:
292
293 "uri": "/api/patient/:patientId"
294
295 "uri": "/api/patient/:patientId/heading/:heading"
296
297
298
299In the first example above, the third part of the API route will be mapped automatically by QEWD to a variable named *patientId*.
300
301In the second example above, the third part of the API route will be mapped automatically by QEWD to a variable named *patientId*, and the fifth part of the API route will be mapped to a variable named *heading*.
302
303These variables are available to you in your handler module as *args[variableName]*.
304
305For example, if your route definition is:
306
307 {
308 "uri": "/api/patient/:patientId/heading/:heading",
309 "method": "GET",
310 "handler": "getPatientHeading",
311 "on_microservice": "clinical_data_service"
312 }
313
314Then, in your *getPatientHeading* handler module, you can access the variables *patientId* and *heading* like this:
315
316
317 module.exports = function(args, finished) {
318 var patientId = args.patientId;
319 var heading = args.heading;
320
321 //.... etc
322
323 };
324
325So, if the user sent the request: *GET /api/patient/1234567/heading/allergies*:
326
327 args.patientId = "1234567"
328 args.heading = "allergies"
329
330
331# Variable MicroService Destinations
332
333The API path variable name *destination* is a reserved name. If specified in an API path, QEWD will attempt to route the request to a MicroService with that name. If the *destination* value does not match a configured MicroService name, then QEWD will return an appropriate error response.
334
335
336For example:
337
338 {
339 "uri": "/api/store/:destination/stockLevels",
340 "method": "GET",
341 "handler": "getStockLevels",
342 "on_microservices: [
343 "london_store",
344 "leeds_store",
345 "edinburgh_store"
346 ],
347 "handler_source": "london_store"
348 }
349
350This can be used to get stock level information from three MicroServices named *london_store*, *leeds_store* and *edinburgh_store*. The *getStockLevels* handler module definition is speficied once only and can be found in the handlers folder for the *london_store* MicroService.
351
352So, for example, a user wanting the stock level information for the Leeds store would send the request:
353
354 GET /api/store/leeds_store/stockLevels
355
356
357# Federated Data Using Group Destinations
358
359In the above example, if we wanted to find out stock levels at all three stores, we'd need to send three separate requests, one for each store MicroService destination.
360
361However, QEWD provides a way of combining MicroServices into a named *Group Destination*. By sending a request for an API that uses such a Group Destination, the Orchestrator automatically sends simultaneous asynchronous requests to all the MicroServices that make up the Group, and returns a composite response once it receives the responses from all the MicroServices in the Group.
362
363If any of the MicroServices in the Group return an error, that error will be included in the composite response.
364
365This mechanism can be used for automatic federation of data across distributed systems.
366
367## Creating a Group Destination
368
369Group destinations are defined in the */configuration/config.json* file.
370
371First, define the individual MicroServices as normal in the *config.json* file, eg:
372
373 {
374 "qewd_up": true,
375 "microservices": [
376 {
377 "name": "london_store",
378 "qewd": {
379 "serverName": "London Store"
380 }
381 },
382 {
383 "name": "leeds_store",
384 "qewd": {
385 "serverName": "Leeds Store"
386 }
387 },
388 {
389 "name": "edinburgh_store",
390 "qewd": {
391 "serverName": "Edinburgh Store"
392 }
393 }
394 ]
395 }
396
397
398Then add a Group destination - we'll call it *all_stores* in this example, but its name is up to you. Define the MicroServices that are included in the Group using the *members* array, for example:
399
400 {
401 "qewd_up": true,
402 "microservices": [
403 {
404 "name": "london_store",
405 "qewd": {
406 "serverName": "London Store"
407 }
408 },
409 {
410 "name": "leeds_store",
411 "qewd": {
412 "serverName": "Leeds Store"
413 }
414 },
415 {
416 "name": "edinburgh_store",
417 "qewd": {
418 "serverName": "Edinburgh Store"
419 }
420 },
421 {
422 "name": "all_stores",
423 "members": [
424 "london_store",
425 "leeds_store",
426 "edinburgh_store"
427 ]
428 }
429 ]
430 }
431
432
433## Create a Route that Uses the Group Destination
434
435Now, in our *routes.json* file we can define a new route for getting the stock levels for all stores:
436
437 {
438 "uri": "/api/all/stockLevels",
439 "method": "GET",
440 "handler": "getStockLevels",
441 "on_microservice": "all_stores",
442 "handler_source": "london_store"
443 }
444
445
446## Try it Out
447
448If you now send a *GET /api/all/stockLevels* request, you should get back a composite result. These are returned in an object named *results*. Each store MicroService will return its results (as returned from the *getStockLevels* handler module) in a sub-object whose name is the MicroService name, eg:
449
450 {
451 "results":{
452 "london_store":{
453 "product":"Widgets",
454 "quantity":34
455 },
456 "leeds_store":{
457 "product":"Widgets",
458 "quantity":35
459 },
460 "edinburgh_store":{
461 "product":"Widgets",
462 "quantity":21
463 }
464 },
465 "token": "eyJ0eXA..."
466 }
467
468The *token* property is always returned with non-Error responses, and is the updated JWT.
469
470**Note 1*: Although the example uses a GET method, you can also use POST, PUT and DELETE to create or modify information in all MicroServices within a Group Destination.
471
472**Note 2*: You can even use a Group Destination name in an API route that has a variable destination.
473
474
475# Dynamically Routed APIs
476
477QEWD-Up is not limited to API routing that is explicitly defined within the *routes.json* route properties.
478
479Sometimes, you need an API whose routing needs to be determined at run-time on the basis of some aspect of its content and/or structure/format. QEWD-Up's Dynamic Routing provides you with the mechanism for doing this.
480
481## Defining a Dynamically-Routed API
482
483A Dynamically-Routed API is specified using the standard *uri* and *method* properties and a third property:
484
485- **router**: The name of a module that you write, whose purpose is to define how to handle it. The module name is up to you.
486
487For example:
488
489 {
490 "uri": "/api/dynamicallyRouted",
491 "method": "GET",
492 "router": "myCustomRouter"
493 }
494
495## Defining a Router Module
496
497In the example above, we're specifying that the routing for a *GET /api/dynamicallyRouted* request will be the responsibility of a module named *myCustomRouter*
498
499This module is created in the *orchestrator* folder, in a sub-folder named *routers*, eg:
500
501 ~/microserviceExample
502 |
503 |_ configuration
504 |
505 |_ orchestrator
506 | |
507 | routers
508 | |
509 | |_ myCustomRouter.js
510
511
512
513## Router Module structure
514
515Your Router Module file should export a function of the structure shown below:
516
517 module.exports = function(args, send, handleResponse) {
518 // Router logic here
519 };
520
521
522## Router Module Arguments
523
524### args
525
526This object contains the incoming request as restructured by QEWD's Master process. Most of the information you'll need for your routing logic is in *args.req*. eg:
527
528- **args.req.headers**: The HTTP request headers
529- **args.req.query**: The parsed name/value pairs in the URL query string (if any)
530- **args.req.body**: The parsed JSON body payload
531
532### handleResponse
533
534Use this function to return the response to the REST Client. It is your responsibility to return the response if you are using a Router module.
535
536It has a single argument: the object containing your response. Note: the response **MUST* be included in a property named *message*.
537
538### send
539
540This is a function you can use to forward an API request. To use it you must first create an object that contains the path and method (and optionally any payload) of an API route that you want to invoke.
541
542**Note**: This API route must be defined within your *routes.json* file.
543
544The *send()* function takes two further arguments:
545
546- args: as provided by the router module interface
547- handleResponse: as described above
548
549## Router Module Example
550
551 module.exports = function(args, send, handleResponse) {
552 if (args.req.query.bypass) {
553 handleResponse({
554 message: {
555 login: 'bypassed'
556 }
557 });
558 }
559 else {
560 var message = {
561 path: '/api/info/info',
562 method: 'GET'
563 };
564 send(message, args, handleResponse);
565 }
566 };
567
568In the example above:
569
570- sending *GET /api/dynamicallyRouted?bypass=true will return a response of {login: 'bypassed'}
571
572- sending *GET /api/dynamicallyRouted* will forward a request for the *GET /api/info/info* API (which must be defined in the *routes.json* file. Its response is returned via the *send()* function's *handleResponse* argument.
573