├── README.md ├── benchmarks ├── README.md ├── test_1.jpg ├── test_1.tiff ├── test_2.jpg ├── test_2.tiff ├── test_3.jpg └── test_3.tiff └── examples └── stormpath.md /README.md: -------------------------------------------------------------------------------- 1 | # REST API Multiple-Request Chaining 2 | 3 | One of the challenges in using RESTful APIs driven by Hypermedia, as well as pulling in numerous and extensive microservices is the requirement to at times make several API calls in order to accomplish the task at hand. Today, that requires numerous HTTP calls as well, which depending on latency, can greatly slow script execution. 4 | 5 | REST API Multiple-Request Chaining is a technique that groups numerous RESTful API calls together in a single HTTP request. 6 | 7 | For example, as shown below, instead of having to do GET calls to `/users/5` and then `/messages/?userId=5` you could instead send a chain to a resource such as `/multirequestchain` that would handle the multiple calls for you. 8 | 9 | REST API Multiple-Request Chaining is setup to allow for conditional calls, as well as provides back only the data you need (instead of all the data that would be returned from each call). This can also be used as a technique for data collection similar to that of GraphQL where instead of embedding objects as models, you're able to make numerous calls at once and get back only the data you request. 10 | 11 | ## A Simple REST API Multiple-Request Chain 12 | Each chain is made of 5 components, all required: 13 | 14 | 15 | Component | Description | Example 16 | ---------- | ----------- | --------- 17 | doOn | DoOn provides the condition on which the call should be performed. Options include "always", a logical if statement `($body.user.firstName == 'Jim')` or an HTTP status code (with * being a wildcard). | 4** 18 | href | The complete path (including querystring) for the call being performed | /users/?lastName=Smith 19 | method | The HTTP method you wish to use to perform the call (such as GET, POST, etc) | get 20 | data | A string or JSON Object of the data you wish to send via the call (typically with POST, PUT, PATCH, DELETE) | { "firstName" : "Jim", "lastName" : "Smith" } 21 | return | The data (as an array) you wish to have returned. If you wish for all data to be returned, use boolean "true" or if you wish for no data to be returned, use boolean "false." | ["firstName", "email", "_links"] 22 | 23 | A simple request where you want to retrieve a user's messages, but first need to make a call to the `/users` resource to obtain the information may look like this: 24 | 25 | ``` 26 | [ 27 | { 28 | "doOn": "always", 29 | "href": "/users/5", 30 | "method": "get", 31 | "data": {}, 32 | "return": [ 33 | "email", "_links" 34 | ] 35 | }, 36 | { 37 | "doOn": 200, 38 | "href" : "$body._links.messages", 39 | "method": "get", 40 | "data": { 41 | "emailAddress": "$body.email" 42 | }, 43 | "return": true, 44 | } 45 | ] 46 | ``` 47 | 48 | ## Multiple Conditional Calls 49 | REST API Multiple-Request Chaining works in chronological order, with each call being considered the next highest priority, or the next natural step in the process. 50 | 51 | The `doOn` specifies whether or not the call should be executed based on the previous call's response, or whether it should be skipped for another call that contains a `doOn` that matches the previous HTTP status code, applies a logical IF statement that returns true, or is specified as "always." In the event that no suitable match is found in the layer of the chain it is currently operating in, the chain will consider this an error and exit - returning back all the data up and including the last call that was attempted. 52 | 53 | For example, in our previous chain - IF the `/users/5` call returned a 404, the chain would have exited, providing you the details of that call, but **NOT** attempting the next call `$body._links.messages` as it required a status code of 200. 54 | 55 | You can also layer multiple conditional calls by placing them in arrays, creating a new layer in the process. However, once the chain reaches the end of its child layers, it will exit - **NOT** iterating through the remaining parent layers. 56 | 57 | ``` 58 | [ 59 | { 60 | "doOn": "always", 61 | "href": "/users/5", 62 | "method": "get", 63 | "data": {}, 64 | "return": [ 65 | "email", "_links" 66 | ] 67 | }, 68 | [ 69 | { 70 | "doOn": 200, 71 | "href" : "$body._links.messages", 72 | "method": "get", 73 | "data": { 74 | "emailAddress": "$body.email" 75 | }, 76 | "return": true, 77 | }, 78 | { 79 | "doOn": "4*|5*", 80 | "href" : "/users", 81 | "method": "post", 82 | "data": { 83 | "firstName": "Jim", 84 | "lastName": "Smith", 85 | "emailAddress": "jim.smith@domain.ext" 86 | }, 87 | "return": true, 88 | }, 89 | [ 90 | { 91 | "doOn": "201", 92 | "href": "$headers.link", 93 | "method": "get", 94 | "data": {}, 95 | "return": [ 96 | "email", "_links" 97 | ] 98 | }, 99 | { 100 | "doOn": 200, 101 | "href" : "$body._links.sendMessage", 102 | "method": "post", 103 | "data": { 104 | "to": "$body.email", 105 | "subject": "Welcome $body.firstName", 106 | "body": "Hello and welcome to our site!" 107 | }, 108 | "return": true, 109 | } 110 | ] 111 | ] 112 | ] 113 | 114 | ``` 115 | 116 | ## Complex IF Statements 117 | The `doOn` property accepts an HTTP Status Code, "always" as a string, or a conditional logical IF statements: 118 | 119 | ##### Simple Equal/ Not Equal 120 | Logic | Example | Meaning 121 | ------ | --------- | ------- 122 | == | $body.firstName == 'Jim' | firstName in Body response is **equal** to Jim 123 | != | $body.firstName != 'Jim' | firstName in Body response is **not equal** to Jim 124 | \>= | $body.age >= 10 | age in Body response is **greater than or equal** to 10 125 | <= | $body.age <= 10 | age in Body response is **less than or equal** to 10 126 | 127 | ##### AND OR STATEMENTS 128 | Logic | Example | Meaning 129 | ------ | --------- | ------- 130 | && | $body.firstName != 'Jim' && $body.age >= 10 | First condition **AND** second condition must be matched 131 | \|\| | $body.firstName != 'Jim' \|\| $body.age >= 10 | Match **EITHER** condition 132 | 133 | ##### REGULAR EXPRESSIONS 134 | Logic | Example | Meaning 135 | ------ | --------- | ------- 136 | regex() | regex('/[a-z]/i', $body.firstName) | **Match** a regular expression 137 | !regex() | regex('/[a-z]/i', $body.firstName) | Does **not match** regular expression 138 | 139 | ### Complex Example: 140 | ``` 141 | [ 142 | { 143 | "doOn": "always", 144 | "href": "/users/5", 145 | "method": "get", 146 | "data": {}, 147 | "return": [ 148 | "firstName", "lastName", "email", "_links" 149 | ] 150 | }, 151 | { 152 | "doOn": "($body.firstName == "Jim" && $body.lastName == "Smith") || regex('/Jim/i', $body.email)", 153 | "href" : "$body._links.messages", 154 | "method": "get", 155 | "data": { 156 | "emailAddress": "$body.email" 157 | }, 158 | "return": true, 159 | } 160 | ] 161 | ``` 162 | 163 | 164 | ## Responses 165 | Because conditional chaining and errors are possible when using REST API Multiple-Request Chaining, the response object needs to return three primary properties: 166 | 167 | Property | Type | Definition 168 | -------- | ---- | ---------- 169 | callsReqested | integer | The number of conditionally applicable calls requested in the chain - this will not necessarily be the total number of calls in the request 170 | callsCompleted | integer | The number of calls that were completed successfully. This helps indicate an error IF the chain had multiple calls, but a condition could not be matched and therefore caused the chain to exit 171 | responses | array | the list of applicable responses to the API chain calls that were performed, including the last call that could either be executed either because the chain was completed or exited due to being unable to meet a necessary condition. 172 | 173 | Within the responses array, each call object needs to include: 174 | 175 | Property | Definition | Example 176 | -------- | ---------- | ------- 177 | href | the full path of the call that was attempted | /users?firstName=Jim 178 | method | the method that was used to make the call | get 179 | status | the HTTP status code the call returned | 200 180 | response | a response object containing the headers (as an object) and the body (as an object or string depending on content-type) | "headers" : { "content-type" : "application/json" }, "body" : { "user" : { "firstName" : "Jim" } } 181 | 182 | 183 | ``` 184 | { 185 | "callsRequested" : 2, 186 | "callsCompleted" : 2, 187 | "responses" : [ 188 | { 189 | "href" : "/users/5", 190 | "method" : "get", 191 | "status" : 200, 192 | "response" : { 193 | "headers" : {}, 194 | "body" : { 195 | "email": "user@user.ext", 196 | "_links": {} 197 | } 198 | } 199 | }, 200 | 201 | { 202 | "href" : "/messages/?userId=5", 203 | "method" : "get", 204 | "status" : 200, 205 | "response" : { 206 | "headers" : {}, 207 | "body" : {} 208 | } 209 | } 210 | ] 211 | } 212 | ``` 213 | 214 | ## FAQs 215 | 216 | ### How do you send headers with REST API Multiple-Request Chaining? 217 | Headers are sent as they always have been, and will automatically be applied to each of the calls requested in the chain. At this time, REST API Multiple-Request Chaining does not support the ability to send specific headers for each call independently - however, if needed, this may certainly be added in the future. 218 | 219 | ### How does this vary from IO State Driven APIs 220 | REST API Multiple-Request Chaining is designed specifically for RESTful APIs utilizing hypermedia over a static or cached state file. This allows for the available paths to be truly dynamic based on the individual calls within the chain, and also prevents the need to make multiple requests to obtain the IO State or available OPTIONS. REST API Multiple-Request Chaining is also designed to expand beyond just the use of a primary key, letting you pull in information from the headers and body of the previous call when performing an action on the next chain. Each link/ call is also conditional, meaning you can specify when that link should be called, and receive the appropriate error response upon a link/ call failure. For more you can see Owen's IO State implemention [here](https://github.com/orubel/grails-api-toolkit-docs/wiki/API-Chaining). 221 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | #Benchmarks 2 | The following are first-look benchmarks for three different series of API calls. They were tested using a basic API-Chaining parser written on PHP. To ensure call fairness, all calls go through the same proxy, hitting the same server to ensure similar server latencies. 3 | 4 | 5 | ### Test 1 6 | Three calls to an API proxy, which in turn then calls a third party service hitting a mock API. This was designed to test the hypermedia links (CPHL format). The Control makes these calls in a traditional manner, making the call, processing the logic, and then moving on to the next call. The Chained call makes one call where the server handles the logic, and then returns back the requests. 7 | 8 | Note - this was run from my local host, meaning there could be slight performance differences from my localhost PHP server (4gb memory) vs the proxy PHP server (512mb memory). 9 | 10 | ![Benchmark one](/benchmarks/test_1.jpg) 11 | 12 | 13 | ### Test 2 14 | Identical to test one except that the second call returns back a failure, forcing the control and the chain to stop after completing the second of the series of requests. The Control makes these calls in a traditional manner, making the call, processing the logic, and then moving on to the next call, finally stopping after finding the second call did not return the expected status code. The Chained call makes one call where the server handles the logic, and then returns back the requests - returning back that only 2 of the 3 `callsRequested` could be completed. 15 | 16 | Note - this was run from my local host, meaning there could be slight performance differences from my localhost PHP server (4gb memory) vs the proxy PHP server (512mb memory). 17 | 18 | ![Benchmark two](/benchmarks/test_2.jpg) 19 | 20 | 21 | ### Test 3 22 | Unlike tests 1 and 2 which involve a third party API, test 3 focused on calls where the server handles all the logic itself. In this case, calls to the server simply return a standard JSON document. The control calls the same resource three times, while the chain sends a single request with a chain that performs an identical function (calling the request three times), but asks the server to only return back the window.image.src (a subnested property). 23 | 24 | Note - this was run from my local host, meaning there could be slight performance differences from my localhost PHP server (4gb memory) vs the proxy PHP server (512mb memory). 25 | 26 | ![Benchmark three](/benchmarks/test_3.jpg) 27 | 28 | 29 | ### Test 4 30 | Test 4 was similar to test 3 in that it called a resource that was strictly handled by the server. However, this time we only made one request, one to the resource itself using our control, and then one to the chain requesting that resource. The purpose of this test was to see the hit that would be taken on an individual call, where again we filtered the response to only return the window.image.src field. 31 | 32 | As expected, the control was faster than the chained request, but surprisingly only by .0014 seconds - demonstrating that at least with the basic parser the actual delay caused by the chain logic was insignificant overall - and easily accounted for when making multiple calls (as demonstrated by the tests above). 33 | -------------------------------------------------------------------------------- /benchmarks/test_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikestowe/REST-API-Multiple-Request-Chaining/c2bef6bae54b4e357656bf4ded13a8f889c7b10d/benchmarks/test_1.jpg -------------------------------------------------------------------------------- /benchmarks/test_1.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikestowe/REST-API-Multiple-Request-Chaining/c2bef6bae54b4e357656bf4ded13a8f889c7b10d/benchmarks/test_1.tiff -------------------------------------------------------------------------------- /benchmarks/test_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikestowe/REST-API-Multiple-Request-Chaining/c2bef6bae54b4e357656bf4ded13a8f889c7b10d/benchmarks/test_2.jpg -------------------------------------------------------------------------------- /benchmarks/test_2.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikestowe/REST-API-Multiple-Request-Chaining/c2bef6bae54b4e357656bf4ded13a8f889c7b10d/benchmarks/test_2.tiff -------------------------------------------------------------------------------- /benchmarks/test_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikestowe/REST-API-Multiple-Request-Chaining/c2bef6bae54b4e357656bf4ded13a8f889c7b10d/benchmarks/test_3.jpg -------------------------------------------------------------------------------- /benchmarks/test_3.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikestowe/REST-API-Multiple-Request-Chaining/c2bef6bae54b4e357656bf4ded13a8f889c7b10d/benchmarks/test_3.tiff -------------------------------------------------------------------------------- /examples/stormpath.md: -------------------------------------------------------------------------------- 1 | #Stormpath Example 2 | **Note this is just an example, Stormpath does not support Multiple-Request Chaining.** 3 | 4 | In this scenario, we want to retrieve a list of accounts where the email is jim@jim.ext. First we must get our current tenant, then our applications (we'll assume we only have one), and then search the accounts for that application. You can see the full list of steps [here](http://docs.stormpath.com/rest/quickstart/). 5 | 6 | We can do this all with a single call, and only have the account information sent back to us (reducing the bandwidth used). 7 | 8 | ``` 9 | [ 10 | { 11 | "doOn": "always", 12 | "href": "/v1/tenants/current", 13 | "method": "get", 14 | "data": {}, 15 | "return": false 16 | }, 17 | { 18 | "doOn": 302, 19 | "href": "${headers.location}/applications", 20 | "method": "get", 21 | "data": {}, 22 | "return": false 23 | }, 24 | { 25 | "doOn": 200, 26 | "href": "${body.items[0].accounts.href}?email=jim@jim.ext", 27 | "method": "get", 28 | "data": {}, 29 | "return": true 30 | } 31 | ] 32 | ``` 33 | --------------------------------------------------------------------------------