Before we start modifying the router functions, we have to consider how to account for multiple models. We currently have two modules for our data model, notes-memory and notes-fs, and we'll be implementing several more by the end of this chapter. We will need a simple method to select between the model being used.
There are several possible ways to do this. For example, in a CommonJS module, it's possible to do the following:
const path = require('path');
const notes = require(process.env.NOTES_MODEL
? path.join('..', process.env.NOTES_MODEL)
: '../models/notes-memory');
This lets us set an environment variable, NOTES_MODEL, to select the module to use for the data model.
This approach does not work with the regular import statement, because the module name in an import statement cannot be such an expression. The Dynamic Import feature now in Node.js does offer a mechanism similar to the snippet just shown.
Dynamic import is an import() function that returns a Promise that will resolve to the imported module. As a function-returning-a-Promise, import() won't be useful as top-level code in the module. But, consider this:
var NotesModule;
async function model() {
if (NotesModule) return NotesModule;
NotesModule = await import(`../models/notes-${process.env.NOTES_MODEL}`);
return NotesModule;
}
export async function create(key, title, body) {
return (await model()).create(key, title, body);
}
export async function update(key, title, body) {
return (await model()).update(key, title, body);
}
export async function read(key) { return (await model()).read(key); }
export async function destroy(key) { return (await model()).destroy(key); }
export async function keylist() { return (await model()).keylist(); }
export async function count() { return (await model()).count(); }
export async function close() { return (await model()).close(); }
Save that module in a file, models/notes.mjs. This module implements the same API as we'll use for all Notes model modules. The model() function is the key to dynamically selecting a notes model implementation based on an environment variable.
This is an async function and therefore its return value is a Promise. The value of that Promise is the selected module, as loaded by import(). Because import() returns a Promise, we use await to know whether it loaded correctly.
Every API method follows this pattern:
export async function methodName(args) {
return (await model()).methodName(args);
}
Because model() returns a Promise, it's most succinct to use an async function and use await to resolve the Promise. Once the Promise is resolved, we simply call the methodName function and go about our business. Otherwise, those API method functions would be as follows:
export function methodName(args) {
return model().then(notes => { return notes.methodName(args); });
}
The two implementations are equivalent, and it's clear which is the more succinct.
With all this awaiting on Promise's returned from async functions, it's worth discussing the overhead. The worst case is on the first call to model(), because the selected notes model has not been loaded. The first time around, the call flow goes as follows:
- The API method calls model(), which calls import(), then await's the module to finish loading
- The API method await's the Promise returned from model(), getting the module object, and it then calls the API function
- The caller is also using await to receive the final result
The first time around, the time is dominated by waiting on import() to finish loading the module. On subsequent calls, the module has already been loaded and the first step is to simply form a resolved Promise containing the module. The API method can then quickly get on with delegating to the actual API method.
To use this, in routes/index.mjs, and in routes/notes.mjs, we make this change:
import util from 'util';
import express from 'express';
import * as notes from '../models/notes';
export const router = express.Router();