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 |
|
21 | Routes are defined in the */configuration/routes.json* file as an array of route objects.
|
22 |
|
23 | These 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 |
|
28 | If 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 |
|
37 | For 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 |
|
43 | For example:
|
44 |
|
45 | [
|
46 | {
|
47 | "uri": "/api/orchestrator/info",
|
48 | "method": "GET",
|
49 | "handler": "getInfo"
|
50 | }
|
51 | ]
|
52 |
|
53 | ## MicroService Applications
|
54 |
|
55 | For 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 |
|
62 | If the API is to be hancled on the Orchestrator, these three properties are sufficient.
|
63 |
|
64 | However, 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 |
|
68 | If 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 |
|
72 | MicroService names (and their physical configurations) are defined in the */configuration/config.json* file.
|
73 |
|
74 | For 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 |
|
88 | If 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 |
|
92 | MicroService names (and their physical configurations) are defined in the */configuration/config.json* file.
|
93 |
|
94 | You 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 |
|
98 | For 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 |
|
116 | You 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 |
|
118 | QEWD-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 |
|
122 | For 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 |
|
135 | In 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 |
|
141 | You can over-ride a number of otherwise automatically-applied steps that are handled by QEWD.
|
142 |
|
143 | ## The *beforeHandler* Hook in Monolithic Applications
|
144 |
|
145 | In 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 |
|
147 | In 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 |
|
149 | To over-ride the *beforeHandler* hook, simply add the *route* property:
|
150 |
|
151 | - **applyBeforeHandler**: *false*
|
152 |
|
153 | For 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 |
|
171 | So 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 |
|
176 | In 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 |
|
178 | Of 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 |
|
182 | For 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 |
|
200 | In 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 |
|
202 | The 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 |
|
220 | In 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 |
|
239 | However, 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 |
|
242 | In 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 |
|
247 | QEWD 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 |
|
253 | The *type* in the error response will actually correspond to the 2nd part of the API path.
|
254 |
|
255 | You 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 |
|
257 | For 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 |
|
281 | Now, 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 |
|
291 | Your 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 |
|
299 | In the first example above, the third part of the API route will be mapped automatically by QEWD to a variable named *patientId*.
|
300 |
|
301 | In 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 |
|
303 | These variables are available to you in your handler module as *args[variableName]*.
|
304 |
|
305 | For 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 |
|
314 | Then, 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 |
|
325 | So, 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 |
|
333 | The 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 |
|
336 | For 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 |
|
350 | This 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 |
|
352 | So, 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 |
|
359 | In 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 |
|
361 | However, 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 |
|
363 | If any of the MicroServices in the Group return an error, that error will be included in the composite response.
|
364 |
|
365 | This mechanism can be used for automatic federation of data across distributed systems.
|
366 |
|
367 | ## Creating a Group Destination
|
368 |
|
369 | Group destinations are defined in the */configuration/config.json* file.
|
370 |
|
371 | First, 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 |
|
398 | Then 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 |
|
435 | Now, 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 |
|
448 | If 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 |
|
468 | The *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 |
|
477 | QEWD-Up is not limited to API routing that is explicitly defined within the *routes.json* route properties.
|
478 |
|
479 | Sometimes, 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 |
|
483 | A 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 |
|
487 | For example:
|
488 |
|
489 | {
|
490 | "uri": "/api/dynamicallyRouted",
|
491 | "method": "GET",
|
492 | "router": "myCustomRouter"
|
493 | }
|
494 |
|
495 | ## Defining a Router Module
|
496 |
|
497 | In 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 |
|
499 | This 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 |
|
515 | Your 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 |
|
526 | This 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 |
|
534 | Use 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 |
|
536 | It has a single argument: the object containing your response. Note: the response **MUST* be included in a property named *message*.
|
537 |
|
538 | ### send
|
539 |
|
540 | This 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 |
|
544 | The *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 |
|
568 | In 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 |
|