microboot

Boot up your app in wee little modules with the help of glob.

Usage no npm install needed!

<script type="module">
  import microboot from 'https://cdn.skypack.dev/microboot';
</script>

README

microboot

Build Status Coverage Status Dependencies npm version

Boot up your app in wee little modules with the help of glob.

const system = await microboot([
  'boot/databases',
  'boot/logging',
  'api/**',
  'listeners'
])

console.log('Service booted!')

Contents

Introduction

microboot is a small helper designed to facilitate booting up complex applications that we all have. Database connections need to be made, logging services initialised and it all ends up happening nastily at the top of the file.

Using microboot helps you organise your start-up by initialising your app step-by-step, reading file structures to grab everything you need.

Uses

I use this tool for booting APIs and microservices within our architecture. microboot lends itself really well to this as each endpoint can be instantiated in a single, isolated file and the only configuration needed to hook these together is microboot.

// index.js
require('microboot')('endpoints')
// connections/api.js
const express = require('express')
const api = express()
api.listen(3000)
module.exports = api
// endpoints/get/list.js
const api = require('../../connections/api')

module.exports = () => {
  api.get('/list', handler)
}

function handler (req, res) => {...}

In that example:

  • index.js (our entry point) triggers microboot to grab everything in the endpoints folder
  • endpoints/get/list.js is found and required
  • connections/api.js is required which set ups a single Express API
  • endpoints/get/list.js adds a GET endpoint
  • :tada:

While this is a very simple example and it could arguably be clearer having everything in a single file, the key here is extensibility. Having each endpoint separated into an individual file means adding or removing endpoints is as simple as adding or removing a file. No extra work needed.

How it works

In your main file, use microboot as it's used above, specifying a single path or multiple paths of files you want to run in the order you want them to run in. Each element in the given array is a different "stage" and files within each are sorted alphabetically to run. Here's our example:

var microboot = require('microboot')

microboot([
  'boot/databases',
  'utils/logging.js',
  'lib/endpoints/**'
]).then((arg) => {
  console.log('Boot complete!')
}).catch((err) => {
  console.error('Something went wrong:', err)
  process.exit(1)
})

In the files you choose to run, ensure they export a function that will be triggered when microboot iterates through.

You can optionally map two parameters: one that's passed through all functions, allowing you to build the object as it goes through and system and the done argument which makes the step asynchronous.

You can also return a promise to make your step asynchronous. Here are some examples:

// boot/databases/mongodb.js
// an asynchronous stage
module.exports = function mongodb (arg, done) {
  connectToMongoDb(function (connection) {
    arg.mongo = connection

    return done()
  })
}
// lib/endpoints/post/login.js
// a synchronous stage
module.exports = function post_login (arg) {
  arg.api.newAppEndpoint('post', '/login')
}
// lib/endpoints/goDb/setup.js
// a promise stage, assuming goDb.setup() returns a promise
module.exports = () => {
  return goDb.setup()
}

You're set! microboot will now first run all JS files found in the boot/databases folder (recursively) including our mongodb.js, then specifically utils/logging.js, then all JS files found in the lib/endpoints folder (recursively) including our login.js.

If you want to know more about the syntax used for specifying recursiveness and the like, take a look at glob; it's what's behind microboot's loader, microloader.

Failing to initialise

If something screws up, you should want to stop your app starting. If that's the case, you can throw an error during a step to stop things in their tracks.

For a synchronous step, just go ahead and throw:

module.exports = function my_broken_api () {
  throw new Error('Oh no! It\'s all gone wrong!')
}

For a callback step, return your error as the first argument of the callback:

module.exports = function my_broken_api (arg, done) {
  startUpApi(function (err) {
    if (err) {
      return done(err)
    }

    return done()
  })
}

For a promise step, either reject your promise or throw if you're running an async function:

module.exports = () => {
  return new Promise((resolve, reject) => {
    reject(new Error('Oh no!'))
  })
}

// or

module.exports = async () => {
  throw new Error('Oh no!')
}

Examples

Yay examples! These all assume the following directory tree, the root representing your project's current working directory.

.
├── bin
│   └── example
├── boot
│   ├── 01_logging
│   │   ├── bunyan.js
│   │   └── postal.js
│   ├── 02_amqp.js
│   └── 03_database.js
├── index.js
├── lib
│   ├── types
│   │   ├── adding.js
│   │   ├── dividing.js
│   │   ├── multiplying.js
│   │   └── subtracting.js
│   └── utils
│       ├── 404.png
│       ├── deleteFile.js
│       ├── doNothing.js
│       ├── getFile.js
│       └── hugFile.js
├── package.json
└── test
    └── test.js

7 directories, 17 files

Running everything in boot

Runs in order: bunyan.js, postal.js, amqp.js, database.js

microboot('boot')

Running everything in boot, then all utils

Runs in order: bunyan.js, postal.js, amqp.js, database.js, deleteFile.js, doNothing.js, getFile.js, hugFile.js

microboot(['boot', 'lib/utils'])

Running 01_logging after 02_amqp.js and 03_database.js

Runs in order: 02_amqp.js, 03_database.js, bunyan.js, postal.js

microboot(['boot/*', 'boot/logging'])

API reference

microboot(stages, [arg], [callback])

  • Arguments

    • stages - A single file path or array of file paths (from the CWD) from which to load Microboot's list of functions to run.
    • arg - Optional, defaults to undefined A single argument that is passed through every function run, allowing you to mutate it to build up the service as it boots. If arg is a function and no callback has been provided, arg will instead be used as the callback.
    • callback(arg) - Optional The function to run once all stages have been successfully run. Is passed the final, mutated arg.
  • Returns

    • If callback defined, undefined.
    • If callback not defined, returns a Promise resolving with arg or rejecting with an Error.

stage([arg], [callback])

  • arg - Optional The arg that's being passed through each stage run. Can be specified in the microboot call or defaults to undefined.
  • callback(err) - Optional If this is mapped to your stage function the stage will be treated as asynchronous and will require that this callback is run before moving to the next one. If there's an error, pass it back as the first parameter. If you wish to return a Promise, return one and do not specify the callback.

Debugging

If microboot doesn't seem to be behaving properly, set the DEBUG=microboot* environment variable, run your app and create a new issue with those logs.