Factory Functions Pattern are a great way to structure applications in NodeJS. This In-Depth article describes how they are used. In short, the advantages of this pattern are:
This post shows how to:
Example screencast how to use hot reloading in PHPStorm or Webstorm:
More complex NodeJS applications can gain maintainability and testability from such a structure.
Dependencies of the app can be wired up in one place.
An example which runs with NodeJS > 4.x looks like this:
// In file ./index.js// Wire up dependenciesconst leveldb = require("leveldb");const express = require("express");const User = require("./model/User")();const userRepository = require("./repository/UserRepository")(leveldb);const userService = require("./service/UserService")(userRepository, User);const userController = require("./controller/UserController")(userService);const app = express();// Attach routesuserController.routes(app);app.listen(3000, () => console.log("Listening on http://localhost:3000"));
To illustrate how injected dependencies can be used, an example implementation of a UserService
could be something like this:
// In file ./src/service/UserService.js// userRepository and User are external dependencies.module.exports = (userRepository, User) => { return { /** * Login a user with the given login provider and user profile. * * @param {string} provider Login provider which was used (google, facebook, ...) * @param {Object} profile User profile from that provider. * @return {User} Logged in user. */ loginWithProvider: (provider, profile) => { return ( userRepository .findByProvider(provider, profile) // If not found, this is a new user. .then((user) => user || User.fromProvider(provider, profile)) .then((user) => user.updateLastLogin()) .then((user) => userRepository.store(user)) ); }, };};
There are two injected dependencies, the userRepository
and a User
model. These dependencies can be accessed in the returned object with currently one function loginWithProvider
. Other function could be added here, they would share the same dependencies.
UserService
A wired up instance of the example UserService
can then be used in other places of the app, like for example
controllers. The same principles applies there:
// In file ./src/controllers/UserController.jsmodule.exports = (userService) => { function login(req, res, next) { return userService .loginWithProvider(req.query.provider, req.query.profile) .then((user) => res.json(user)) .catch(next); } return { routes: (app) => { app.get("/login", login); }, };};
Now all dependencies are injected and wired up at one place. Nice!
When apps grow, the startup cost increases due to parsing javascript files
and making connections to databases and servies. Hot reloading NodeJS apps avoids the startup costs and
lets you iterate quicker. To make this possible the previous structure needs a little refactoring.
We have to move out the dependencies into another file which can be reloaded on file changes.
The dependencies then need to be exported to be used:
// In ./src/dependencies.js// Static dependencies, changing these requires a restart// in hot reloading mode.const level = require("level");const leveldb = require("level-promisify")(level);const express = require("express");const app = express();const deps = { // Dynamic dependencies which will be hot reloaded. // Use ES6 getters to require when needed. app, get routes() { deps.userController.routes(deps.app); return app; }, get User() { return require("./model/User")(); }, get userRepository() { return require("./repository/UserRepository")(leveldb); }, get userService() { return require("./service/UserService")(deps.userRepository, deps.User); }, get userController() { return require("./controller/UserController")(deps.userService); },};module.exports = deps;
This is how the ./index.js
looks like after the refactoring:
// In file ./index.js// Start with hot reloading: node index hotreloadconst isHot = process.argv[2] === "hotreload";const app = isHot ? require("./hotreload") : require("./src/server");app.listen(3000, () => console.log("Listening on http://localhost:3000"));
// In file ./src/server.jsconst dependencies = require("./dependencies");const app = dependencies.app;const routes = dependencies.routes;app.use(routes);module.exports = app;
For hot reloading, create a new file:
// In file ./src/hotreload.jsconst chokidar = require("chokidar");const dependencies = require("./src/dependencies");const app = dependencies.app;// Include server routes as a middlewareapp.use((req, res, next) => { // Require all dependencies. dependencies.routes(req, res, next);});// Do "hot-reloading" of express stuff on the server// Throw away cached modules and re-require next time// Ensure there's no important state in there!const watcher = chokidar.watch(["./src"]);watcher.on("ready", () => { watcher.on("all", () => { console.log("Clearing module cache from server"); Object.keys(require.cache).forEach((file) => { if (/src/.test(file) && file.indexOf("node_modules") === -1) { delete require.cache[file]; } }); });});module.exports = app;
A complete example can be found on github.