In the first half of this chapter, you developed APIs for discovering and returning books based on a variety of search criteria. In this second half, you’ll be creating APIs for manipulating book bundles. Recall that a book bundle has a name and maintains a collection of related books.
Here’s an example of a book bundle:
| | { |
| | "name": "light reading", |
| | "books": [{ |
| | "id": "pg132", |
| | "title": "The Art of War" |
| | },{ |
| | "id": "pg2680", |
| | "title": "Meditations", |
| | },{ |
| | "id": "pg6456", |
| | "title": "Public Opinion" |
| | }] |
| | } |
Creating these APIs will be programmatically more intensive than creating the search APIs because they require more back-and-forth between your Node.js service and the underlying datastore. For example, consider an API to update the name of a bundle. Roughly speaking, your Node.js code will need to do the following:
In addition to handling these asynchronously and in order, you’ll have to deal with various failure modes. What if Elasticsearch is down? What if the bundle doesn’t exist? What if the bundle changed between the time Node.js downloaded it and the time it reuploaded it? What if Elasticsearch fails to update for some other reason?
Some of these considerations were already covered while creating the search APIs, but you get the point. There are a lot of ways a sequence of asynchronous events could fail midstride, and it’s important to think about what kind of response you should provide to users of your API. What HTTP status code is closest to explaining the situation? What kind of error message should you present?
We’ll cover much of this as we go along, so let’s get to it. To begin, create a file called lib/bundle.js.
| | /** |
| | * Provides API endpoints for working with book bundles. |
| | */ |
| | 'use strict'; |
| | const rp = require('request-promise'); |
| | |
| | module.exports = (app, es) => { |
| | |
| | const url = `http://${es.host}:${es.port}/${es.bundles_index}/bundle`; |
| | |
| | }; |
Next, open your server.js and use require to bring in the bundle API.
| | require('./lib/bundle.js')(app, nconf.get('es')); |
To create a new resource using a RESTful API, the right HTTP method to use is POST. Recall that POST is good for creating a resource when you don’t know the URL where that resource will reside.
This is the case for the book bundles. Although each bundle has a name parameter, these are not guaranteed to be unique, and so they don’t make good identifiers. It’s better to let Elasticsearch automatically generate an identifier for each bundle as it’s created.
Add this code to your module.exports function for creating a book bundle:
| | /** |
| | * Create a new bundle with the specified name. |
| | * curl -X POST http://<host>:<port>/api/bundle?name=<name> |
| | */ |
| | app.post('/api/bundle', (req, res) => { |
| | const bundle = { |
| | name: req.query.name || '', |
| | books: [], |
| | }; |
| | |
| | rp.post({url, body: bundle, json: true}) |
| | .then(esResBody => res.status(201).json(esResBody)) |
| | .catch(({error}) => res.status(error.status || 502).json(error)); |
| | }); |
To begin, we’re using app.post rather than app.get as with previous APIs. This means Express will use this handler only when the incoming HTTP request is using the POST method. This API does not take any named route parameters, but does expect one optional query parameter called name.
Inside the callback, first we construct the bundle object, which consists of a name field (which may be the empty string), and an initially empty list to hold the books that will be added to the bundle.
Next, we use rp.post to fire off a POST request to Elasticsearch, passing it the JSON-encoded bundle object we just created. rp.post returns a Promise, to which we attach success and failure callbacks using .then() and .catch(). This is the same pattern we used in the /suggest API earlier, but instead of 200 OK we return a 201 Created HTTP status code. Once you save the lib/bundle.js file, it should be ready to try out. Open a terminal and use curl to create a bundle.
| | $ curl -s -X POST localhost:60702/api/bundle?name=light%20reading | jq '.' |
| | { |
| | "_index": "b4", |
| | "_type": "bundle", |
| | "_id": "AVuFkyXcpWVRyMBC8pgr", |
| | "_version": 1, |
| | "result": "created", |
| | "_shards": { |
| | "total": 2, |
| | "successful": 1, |
| | "failed": 0 |
| | }, |
| | "created": true |
| | } |
Note the _id field. This is automatically generated by Elasticsearch for the new bundle document that was just created. Copy this string, as you’ll need it for the remaining examples in this chapter. It may help to put it in an environment variable for easy retrieval.
| | $ BUNDLE_ID=AVuFkyXcpWVRyMBC8pgr |
| | $ echo $BUNDLE_ID |
| | AVuFkyXcpWVRyMBC8pgr |
Using curl, we can hit Elasticsearch directly to check on this bundle document.
| | $ curl -s localhost:9200/b4/bundle/$BUNDLE_ID | jq '.' |
| | { |
| | "_index": "b4", |
| | "_type": "bundle", |
| | "_id": "AVuFkyXcpWVRyMBC8pgr", |
| | "_version": 1, |
| | "found": true, |
| | "_source": { |
| | "name": "light reading", |
| | "books": [] |
| | } |
| | } |
In the next section, we’ll add an API to perform exactly this kind of lookup so we don’t have to go to Elasticsearch directly. While doing that, we’ll explore one of the most exciting new features of Node.js 8: async functions.