3

Manage Infrastructure With Node.js

 1 year ago
source link: https://devm.io/nodejs/javascript-nodejs-opensource-001
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Tutorial: Node.js — part 4

Manage Infrastructure With Node.js


Anyone developing an application in Node.js not only has to deal with the code itself. It also involves connecting infrastructure, including databases, and the execution layer with Docker. How does that work?

In the past three parts of this series, we created an application written in Node.js that provides an API for managing a simple task list. The API deliberately does not use REST, but only separates writing from reading. Writing operations are based on POST, and reading on GET. The actual semantics were shifted to the path of the URL, so that the technical intention is preserved and the API is far more comprehensible than if it were to rely solely on the four technical verbs provided in REST. The current version of the application contains three routes, one for noting and checking off tasks, the other for listing all unfinished tasks:

  • POST /note-todo
  • POST /tick-off-todo
  • GET /pending-todos

The separation of write and read operations is also continued in the code, and the technical designations are also found here. A distinction is made between functions that change the state of the application and functions that read and return the current state. The application implements a simple form of the CQRS design pattern [1], which recommends this separation.

From in-memory to database

Data storage is currently RAM-based in an array. The core is the file ./lib/Todos.js, in which the class Todos is implemented. Its structure initializes an array, which is then modified by the noteTodo and tickOffTodo functions and read by getPendingTodos. Since the functions are already marked as async, it is easy to replace this class with an equivalent variant that accesses a database.

For this purpose, the application will be extended in a plug-in-based manner so that you can decide at startup which data storage to use. It’s helpful that there is only one instance of the Todos class in the entire application, which is created globally in the ./lib/getApp.js file and then only passed on to other functions. The first step is to rename the Todos class to InMemoryTodos, rename the file, and move it to the new ./lib/stores directory.

Next, the ./lib/getApp.js file needs to be adjusted to access the new file and instantiate the renamed class. Apart from adjusting the call to the require function, this only affects a single line:

const todos = new InMemoryTodos();

Next, it’s necessary to ensure that the desired store can be selected and configured from the outside. To ensure that all stores can be created in the same way, a new object is introduced as a parameter in the InMemoryTodos con-structure, which can be used to pass any data. Since the InMemoryTodos class does not expect any parameters, the object remains empty at this point, as Listing 1 shows.

Listing 1

class InMemoryTodos {
  constructor ({}) {
    this.items = [];
  }


  // ...
}

At first glance, this change seems pointless, but it ensures that a store must always be called with an object and no special handling is required for stores that do not expect parameters. The call in the ./lib/getApp.js file changes as follows:

const todos = new InMemoryTodos({});

However, the actual parameters should not be hardcoded in the code. With a real database, this would not make sense, since you would want to be able to change the connection string from the outside and it is not advisable to store sensitive data such as access data in your code. Instead, the parameters should be set via the environment variable STORE _OPTIONS. Additionally, the type of store should be configurable by the environment variable STORE _TYPE. The in-memory store and empty object serve as default values as parameters.

To do this, the ./app.js file must be adapted, in which the port is already read from an environment variable. It is convenient that the processenv module converts the contents of the environment variable into the appropriate type, in the case of STORE_ OPTIONS into the type object. Then the two parameters must be passed to the getApp function (Listing 2).

Listing 2

'use strict';


const getApp = require('./lib/getApp');
const http = require('http');
const { processenv } = require('processenv');


(async () => {
  const port = processenv('PORT', 3_000);


  const storeType = processenv('STORE_TYPE', 'InMemory');
  const storeOptions = processenv('STORE_OPTIONS', {});


  const server = http.createServer(await getApp({ storeType, storeOptions }));


  server.listen(port);
})();

There, the two parameters must be received and evaluated. To do this, all available stores are stored in a dictionary called todos, then the appropriate entry is selected based on the value of storeType and called with storeOptions. It was a good idea to perform the initialization in the asynchronous function initialize, since some databases require asynchronous initialization, as can be seen in Listing 3.

Listing 3

// ...


const getApp = async function ({ storeType, storeOptions }) {
  const Todos = {
    InMemory: InMemoryTodos
  };


  const todos = new Todos[storeType](storeOptions);


  await todos.initialize();


  // ...


  return app;
};


module.exports = getApp;

With that, everything is ready to go. Adding support for a concrete database is limited to writing the appropriate store, storing it in ./lib/stores/, importing it into ./lib/getApp.js, and registering it in Todos. Everything else can be controlled from the outside. For example, if you want to support a MongoDB database, you first need to install the mongodb module:

$ npm install mongodb

Then, the ./lib/stores/MongoDbTodos.js file must be created, which contains the class visible in Listing 4. Before MongoDB can be used, the new type must be imported and registered in the ./lib/getApp.js file. While only a common require is needed for the import, the registration is done by adding the new class to the Todos listing:

const Todos = {
  InMemory: InMemoryTodos,
  MongoDb: MongoDbTodos
...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK