The version of the NGINX JavaScript module released with NGINX Plus R15 can now issue subrequests, meaning that requests can be initiated in JavaScript code. This enables a whole new set of use cases to be addressed.
One of these use cases is batching API requests so that a single API request from a client can be turned into multiple API requests to a set of backend servers, and the responses aggregated into a single response to the client. In this blog we’re using an ecommerce site as the example – when the client requests information about a particular product, subrequests are made to three backend services: catalog, inventory, and customer reviews.
This post builds on the subrequest example in the NGINX Plus R15 announcement, which shows how to use subrequests to send the same request to two backend servers, and return only the first response to the client.
[Editor – This post is one of several that explore use cases for the NGINX JavaScript module. For a complete list, see Use Cases for the NGINX JavaScript Module.]
Introduction
The goal of this post is to provide working examples of API request batching that are simple and straightforward. The examples meet all of the following requirements:
- The HTTP
GET
method is used for all requests. - All responses are in JSON format.
-
Two styles of API requests are supported:
- The argument to the API program is the final element of the URI. We call this the final‑element request style. In our example, a client request for /batch-api/product/14286 spawns requests to /myapi/catalog/14286, /myapi/inventory/14286, and /myapi/review/14286.
- The argument is identified in the query string of the URI. We call this the query‑string request style. In the example, a request for /batch-api2/product?item=14286 spawns requests to /myapi/catalog.php?item=14286, /myapi/inventory.php?item=14286, and /myapi/review.php?item=14286.
Note that with either style there can be additional arguments in the query string.
- The set of subrequests triggered by a client request is dynamically configurable, meaning we can change it without manually editing the NGINX Plus configuration and reloading it.
- The API requests to the backend servers are independent of one another.
Solution Overview
In addition to using the new subrequest feature of NGINX JavaScript, this solution utilizes NGINX Plus’ key‑value store feature, allowing configuration changes to be made dynamically with the NGINX Plus API.
The following graphics illustrate the two supported request styles.
Final‑element request style:
Query‑string request style:
In both examples, the client needs to get information about a product from the catalog, inventory, and review services. It sends one request to NGINX Plus, either passing the item number as part of the URI or in the query string. Requests are then sent to the three services and the responses are aggregated into a single response to the client.
There are two components required to get this working with NGINX Plus: a JavaScript program and the NGINX Plus configuration.
The JavaScript Program
The JavaScript function batchAPI()
is called for each client request. It gets the comma‑separated list of API URIs from the key‑value store and iterates through them, making a subrequest for each, and specifying the callback function done()
to process each response. For final‑element style requests, the last element of the URI, stored in the NGINX variable $uri_suffix
, is passed as the query string of each subrequest, along with any other query‑string arguments in the original client URI. For query‑string style requests, the query string from the client requests is passed as the query string of each subrequest.
The calls are made in the order they are listed in the key‑value store, but are asynchronous, so the responses can come back in any order. The responses are aggregated into one response to the client in the order in which they are received. Here is a minimal version of the JavaScript program:
A version of the JavaScript program with more extensive error handling, logging, and comments is available in the GitHub Gist repo as batch-api.js.
Minimal NGINX Plus Configuration
The following NGINX Plus configuration implements the two request styles discussed above, using a group of upstream servers. To keep things simple, this configuration assumes that the upstream servers are able to translate a call of the form /myapi/service/item# (final‑element style) to /myapi/service.php/?item=item# (query‑string style), which is the “native” URI format for PHP, the language for our sample catalog, inventory, and review services.
For a more extensive NGINX Plus configuration that performs the translation itself, see Expanding the NGINX Configuration to Translate URIs below.
Let’s look at the configuration file section by section and explain what each part does:
-
Specify the JavaScript file:
-
Define a key‑value store to hold the list of API requests where the last element of the URI identifies the argument to the API program. The key uses the
$uri_prefix
variable to capture the portion of the URI before the last /:In NGINX Plus R16 and later, you can take advantage of two additional key‑value features:
- Set an expiration time for the entries in a key‑value store, by adding the
timeout
parameter to thekeyval_zone
directive. For example, to have each batched URI expire daily, addtimeout=1d
. - Synchronize the key‑value store across a cluster of NGINX Plus instances, by adding the
sync
parameter to thekeyval_zone
directive. You must also include thetimeout
parameter in this case.
For instructions on setting up synchronization for key-value stores, see the NGINX Plus Admin Guide.
- Set an expiration time for the entries in a key‑value store, by adding the
-
Define another key‑value store for APIs where the argument is identified in the query string. Here the key (
$uri
) captures the entire URI, not including the query string: -
Define two maps to split the URI into two parts for APIs that accept requests where the argument is the last element of the URI. The
$uri_prefix
variable captures the URI elements before the last / and$uri_suffix
captures the final element: -
Define the upstream group of API servers:
-
Define the virtual server that handles client requests and subrequests:
-
Define a location to handle client requests that use the final‑element style, which is indicated to the
batchAPI
function by setting the$batch_api_arg_in_uri
variable toon
: -
Define a location to handle client requests that use the query‑string style, which is indicated to the
batchAPI
function by setting the$batch_api_arg_in_uri
variable tooff
: -
Define the location that handles the subrequests:
-
Enable the NGINX Plus API so that data can be maintained in the key‑value store:
Here’s the complete configuration:
Configuring the Batched API Requests
We’re using the NGINX Plus key‑value store feature to map inbound client requests to a set of API requests to execute. The configuration in the previous section defines separate key‑value stores for the two request styles:
- The key‑value store for the final‑element style is named batch_api and the key is the
$uri_prefix
variable, which captures the elements before the final / of the request URI. - The key‑value store for the query‑string style is named batch_api2 and the key is the
$uri
variable, which captures the complete request URI without the query string.
In both key‑value stores, the value associated with each key is a comma‑separated list of URIs that are made available at runtime in the $batch_api
or $batch_api2
variables.
For final‑element style requests, we want to map a client request for /batch-api/product to requests to the three services /myapi/catalog, /myapi/inventory, and /myapi/review, such that a request for /batch-api/product/14286 results in requests to /myapi/catalog/14286, /myapi/product/14286 and /myapi/review/14286.
To add these mappings to the batch_api key‑value store, we send the following request to the NGINX Plus instance running on the local machine:
$ curl -iX POST -d '{"/batch-api/product":"/myapi/catalog,/myapi/inventory,/myapi/review"}' http://localhost/api/3/http/keyvals/batch_api
For the query‑string style requests, we want to map a client request for /batch-api2/product to requests to the three services /myapi/catalog.php, /myapi/inventory.php, and /myapi/review.php, such that a request for /batch-api2/product?item=14286 results in requests to /myapi/catalog?item=14286, /myapi/product?item=14286 and /myapi/review?item=14286.
To add these mappings to the batch_api2 key‑value store, we send this request:
$ curl -iX POST -d '{"/batch-api2/product":"/myapi/catalog.php,/myapi/inventory.php,/myapi/review.php"}' http://localhost/api/3/http/keyvals/batch_api2
Running the Examples
The same PHP pages handle requests made in both request styles. For simplicity, we assume the backend servers are able to translate final‑element style URIs (/myapi/service/item#) to query‑string style URIs (/myapi/service.php?item=item#). It’s not necessary to translate query‑string style URIs, as that is the “native” URI format for PHP programs.
All of the services return a JSON‑formatted response that includes the service name and item argument. For example, a request for /myapi/catalog.php?item=14286 results in the following response from catalog.php:
{"service":"Catalog","item":"14286"}
Although the PHP programs used in these examples include the service name in the response, this might not be the case for all services. To help match responses to the services that generated them, the JavaScript program includes the URI in the aggregated response.
For example, the aggregated response for a request for /batch-api/product/14286 is as follows (although the order of the three component responses might vary):
[["/myapi/review/14286",{"service":"Review","item":"14286"}],["/myapi/inventory/14286",{"service":"Inventory","item":"14286"}],["/myapi/catalog/14286",{"service":"Catalog","item":"14286"}]]
Expanding the NGINX Plus Configuration to Translate URIs
As mentioned, the minimal NGINX Plus configuration discussed above assumes that the API servers are able to translate URIs of the format /myapi/service/item# to /myapi/service.php/?item=item#. We can also implement the translation in the NGINX Plus configuration by making the following changes.
-
We add a new upstream group to receive the subrequests:
-
We add a new virtual server to listen for requests received by the services upstream group. When query‑string style requests are received, they are simply proxied to the api_servers upstream group because the URI already has the .php extension. When final‑element style requests are received, the URI is rewritten so that the last element in the URI is removed and converted to a query‑string argument:
-
We change the /myapi location to proxy to the new upstream group:
Here’s the complete configuration:
Additional Components
The sample JavaScript programs and NGINX Plus configurations can be adapted to other styles of APIs, but if you want to test using all the components used to create this blog post, they are available in our Gist repo on GitHub, including the NGINX Unit configuration for serving the PHP pages:
batch-api.js
catalog.php
inventory.php
review.php
unit.config
Conclusion
These examples show how to batch API requests and provide an aggregated response to the client. The NGINX Plus key‑value store allows us to configure the API requests dynamically with the NGINX Plus API. API systems vary quite widely in their exact operations so there are many enhancements that can be made to this example to fit the needs of a particular API.
You can start testing NGINX JavaScript subrequests with NGINX Open Source, but if you want to try the API batching examples or test other use cases that take advantage of the NGINX Plus features like the key‑value store and NGINX Plus API, request a 30-day free trial and start experimenting.