Table of Contents for
Node.js 8 the Right Way

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition Node.js 8 the Right Way by Jim Wilson Published by Pragmatic Bookshelf, 2018
  1. Title Page
  2. Node.js 8 the Right Way
  3. Node.js 8 the Right Way
  4. Node.js 8 the Right Way
  5. Node.js 8 the Right Way
  6.  Acknowledgments
  7.  Preface
  8. Why Node.js the Right Way?
  9. What’s in This Book
  10. What This Book Is Not
  11. Code Examples and Conventions
  12. Online Resources
  13. Part I. Getting Up to Speed on Node.js 8
  14. 1. Getting Started
  15. Thinking Beyond the web
  16. Node.js’s Niche
  17. How Node.js Applications Work
  18. Aspects of Node.js Development
  19. Installing Node.js
  20. 2. Wrangling the File System
  21. Programming for the Node.js Event Loop
  22. Spawning a Child Process
  23. Capturing Data from an EventEmitter
  24. Reading and Writing Files Asynchronously
  25. The Two Phases of a Node.js Program
  26. Wrapping Up
  27. 3. Networking with Sockets
  28. Listening for Socket Connections
  29. Implementing a Messaging Protocol
  30. Creating Socket Client Connections
  31. Testing Network Application Functionality
  32. Extending Core Classes in Custom Modules
  33. Developing Unit Tests with Mocha
  34. Wrapping Up
  35. 4. Connecting Robust Microservices
  36. Installing ØMQ
  37. Publishing and Subscribing to Messages
  38. Responding to Requests
  39. Routing and Dealing Messages
  40. Clustering Node.js Processes
  41. Pushing and Pulling Messages
  42. Wrapping Up
  43. Node.js 8 the Right Way
  44. Part II. Working with Data
  45. 5. Transforming Data and Testing Continuously
  46. Procuring External Data
  47. Behavior-Driven Development with Mocha and Chai
  48. Extracting Data from XML with Cheerio
  49. Processing Data Files Sequentially
  50. Debugging Tests with Chrome DevTools
  51. Wrapping Up
  52. 6. Commanding Databases
  53. Introducing Elasticsearch
  54. Creating a Command-Line Program in Node.js with Commander
  55. Using request to Fetch JSON over HTTP
  56. Shaping JSON with jq
  57. Inserting Elasticsearch Documents in Bulk
  58. Implementing an Elasticsearch Query Command
  59. Wrapping Up
  60. Node.js 8 the Right Way
  61. Part III. Creating an Application from the Ground Up
  62. 7. Developing RESTful Web Services
  63. Advantages of Express
  64. Serving APIs with Express
  65. Writing Modular Express Services
  66. Keeping Services Running with nodemon
  67. Adding Search APIs
  68. Simplifying Code Flows with Promises
  69. Manipulating Documents RESTfully
  70. Emulating Synchronous Style with async and await
  71. Providing an Async Handler Function to Express
  72. Wrapping Up
  73. 8. Creating a Beautiful User Experience
  74. Getting Started with webpack
  75. Generating Your First webpack Bundle
  76. Sprucing Up Your UI with Bootstrap
  77. Bringing in Bootstrap JavaScript and jQuery
  78. Transpiling with TypeScript
  79. Templating HTML with Handlebars
  80. Implementing hashChange Navigation
  81. Listing Objects in a View
  82. Saving Data with a Form
  83. Wrapping Up
  84. 9. Fortifying Your Application
  85. Setting Up the Initial Project
  86. Managing User Sessions in Express
  87. Adding Authentication UI Elements
  88. Setting Up Passport
  89. Authenticating with Facebook, Twitter, and Google
  90. Composing an Express Router
  91. Bringing in the Book Bundle UI
  92. Serving in Production
  93. Wrapping Up
  94. Node.js 8 the Right Way
  95. 10. BONUS: Developing Flows with Node-RED
  96. Setting Up Node-RED
  97. Securing Node-RED
  98. Developing a Node-RED Flow
  99. Creating HTTP APIs with Node-RED
  100. Handling Errors in Node-RED Flows
  101. Wrapping Up
  102. A1. Setting Up Angular
  103. A2. Setting Up React
  104. Node.js 8 the Right Way

Saving Data with a Form

It’s great that our application is able to list bundles that already exist, but what about adding new ones? The right tool for this job is the venerable HTML <form> tag. Using a form, we can capture user input and process it by making a proxied call to the back-end APIs. Since the form is static HTML, we’ll put it in app/templates.ts. Open that file now and add this template.

 export​ ​const​ addBundleForm = Handlebars.compile(​`
  <div class="panel panel-default">
  <div class="panel-heading">Create a new bundle.</div>
  <div class="panel-body">
  <form>
  <div class="input-group">
  <input class="form-control" placeholder="Bundle Name" />
  <span class="input-group-btn">
  <button class="btn btn-primary" type="submit">Create</button>
  </span>
  </div>
  </form>
  </div>
  </div>
 `​);

Like the listBundles template, the addBundleForm template creates a Bootstrap panel to house the content. Inside the panel, we create a <form> with an <input> where the user can enter a new bundle name, and a <button> to submit the form.

Next, open your app/index.ts for editing and head to the listBundles. We need to add the form to this and handle form submission.

 const​ listBundles = bundles => {
  mainElement.innerHTML =
  templates.addBundleForm() + templates.listBundles({bundles});
 
 const​ form = mainElement.querySelector(​'form'​);
  form.addEventListener(​'submit'​, event => {
  event.preventDefault();
 const​ name = form.querySelector(​'input'​).value;
  addBundle(name);
  });
 };

At the top of this function, we set the mainElement’s HTML to include both the form and the bundle-listing table. After that, we grab a reference to the <form> element and capture its submit event.

The browser’s default behavior when a form is submitted is to navigate away from the current page while submitting the form data to the server. Inside the submit event handler, the first thing we have to do is call event.preventDefault to stop this from happening. After that, we extract the name from the form’s <input> tag, and call the as-yet-unwritten addBundle.

To complete the functionality, we need to introduce the addBundle async function, which takes the name of a bundle to add and asynchronously adds it, then updates the list. Whether this operation succeeds or fails, we’ll need to inform the user, so add this convenience function for showing an alert to the user:

 /**
  * Show an alert to the user.
  */
 const​ showAlert = (message, type = ​'danger'​) => {
 const​ html = templates.alert({type, message});
  alertsElement.insertAdjacentHTML(​'beforeend'​, html);
 };

This simple helper function uses the alert Handlebars template from earlier to generate a nice-looking dismissable alert box. We inject the resulting HTML at the end of the alertsElement using the insertAdjacentHTML method.

Finally, still inside app/index.ts, add the following addBundle async function.

 /**
  * Create a new bundle with the given name, then list bundles.
  */
 const​ addBundle = ​async​ (name) => {
 try​ {
 // Grab the list of bundles already created.
 const​ bundles = ​await​ getBundles();
 
 // Add the new bundle.
 const​ url = ​`/api/bundle?name=​${encodeURIComponent(name)}​`​;
 const​ res = ​await​ fetch(url, {method: ​'POST'​});
 const​ resBody = ​await​ res.json();
 
 // Merge the new bundle into the original results and show them.
  bundles.push({id: resBody._id, name});
  listBundles(bundles);
 
  showAlert(​`Bundle "​${name}​" created!`​, ​'success'​);
  } ​catch​ (err) {
  showAlert(err);
  }
 };

In form, this code shouldn’t appear too surprising. It’s the same sort of async function you’ve been developing in this chapter and the previous one, with a big try/catch block to handle both synchronous exceptions and rejected Promises.

First, we await an updated list of bundles from the async function getBundles. We have to get an updated list because it may have changed since the form was originally rendered on the page (for example, if the user took action in another tab).

Next, we issue an HTTP POST request using fetch to create a bundle with the user-specified name. Once that returns, we extract the JSON response, which gives us access to the ID that Elasticsearch generated for the bundle.

If everything was successful to this point, we add that bundle to the bundles array and then call listBundles to re-render the table. And lastly, we use the showAlert function to inform the user that the operation was successful. If anything went wrong, we use showAlert to indicate the problem in the catch block at the bottom.

You might wonder why we don’t just add the bundle and then request the full bundle list and display it. The reason is something called eventual consistency. Elasticsearch, like many database solutions, experiences a delay between successful changes to the data and those results showing up in subsequent queries. Consider this bad implementation of addBundle:

 // BAD IMPLEMENTATION! Subject to stale data due to eventual consistency.
 const​ addBundle = ​async​ (name) => {
 try​ {
 // Add the new bundle.
 const​ url = ​`/api/bundle?name=​${encodeURIComponent(name)}​`​;
 const​ res = ​await​ fetch(url, {method: ​'POST'​});
 const​ resBody = ​await​ res.json();
 
 // Grab the list of all bundles, expecting the new one to be in the list.
 // Due to eventual consistency, the new bundle may be missing!
 const​ bundles = ​await​ getBundles();
  listBundles(bundles);
 
  showAlert(​`Bundle "​${name}​" created!`​, ​'success'​);
  } ​catch​ (err) {
  showAlert(err);
  }
 };

In this bad implementation, we POST the new bundle first, then immediately invoke getBundles, expecting it to contain all the bundles, including the brand-new one. Now, the ordering of the requests isn’t itself a problem. Since we’re using await at each step, the next fetch request won’t start until the previous one has finished.

However, there’s very little time between the finishing of the POST and the request to get bundles. That means it’s possible—and, in my experience, probable—that the result of the getBundles request won’t include the bundle that was just added.

Unfortunately, this experience is not unique to Elasticsearch. In order to handle requests at scale, many systems distribute the load across a network. Even SQL servers are not immune. One common practice is to have a central server receive write requests, then replicate data to other read-only servers to satisfy queries. Even if the delay between a central write and the replication is tiny, it’s still quite possible to perform a read too soon after a write to pick up the changes.

For this reason, it’s up to you to maintain the relevant data in your application and mirror the changes you’re making asynchronously upstream. That’s the safe way to guard against getting stale results due to eventual consistency.

Once you save these changes to app/index.ts, you should see the form above the list when you visit #list-bundles. Try adding a new bundle. It should appear in the list, with a success message up top.

images/ux-addbundle.png

Whew! What a lot of work to get this far. There is, of course, a ton more functionality this application needs to be usable, but a lot of it is in the form of iterative features and improvements on this basic structure.

Let’s quickly recap what we discussed in this chapter, then move on to wrapping it all together.