JavaScript Functions, Standardized

Every JavaScript developer knows the friction of consuming an unfamiliar npm package. You read the docs, guess at the API, and hope the function signatures match your expectations. The machine specification, authored by Mike McNeil, aims to remove that guesswork by defining a universal, predictable interface for JavaScript functions.

Machines are self-documenting functions built to this specification. Each machine has one clear purpose — sending an email, issuing a JSON Web Token, making a fetch request — and its structure makes both its behavior and its inputs obvious at a glance. This predictability extends to debugging: because every machine follows the same contract, tracing issues becomes straightforward.

Machines are already in wide circulation. If you've used the Sails.js MVC framework, you've interacted with them (McNeil authored both Sails.js and the machine spec). You can browse published machinepacks on npm by searching for the machinepack prefix, or visit the registry at node-machine.org/machinepacks, which syncs with npm and updates every ten minutes.

The Anatomy of a Machine

At its core, a machine is an exported object with standardized properties and a single function. Here's a representative example from the official spec:

    module.exports = {
  friendlyName: 'Do something',
  description: 'Do something with the provided inputs that results in one of the exit scenarios.',
  extendedDescription: 'This optional extended description can be used to communicate caveats, technical notes, or any other sort of additional information which might be helpful for users of this machine.',
  moreInfoUrl: 'https://stripe.com/docs/api#list_cards',
  sideEffects: 'cacheable',
  sync: true,

  inputs: {
    brand: {
      friendlyName: 'Some input',
      description: 'The brand of gummy worms.',
      extendedDescription: 'The provided value will be matched against all known gummy worm brands. The match is case-insensitive, and tolerant of typos within Levenstein edit distance <= 2 (if ambiguous, prefers whichever brand comes first alphabetically).',
      moreInfoUrl: 'https://gummy-worms.org/common-brands?countries=all',
      required: true,
      example: 'haribo',
      whereToGet: {
        url: 'https://gummy-worms.org/how-to-check-your-brand',
        description: 'Look at the giant branding on the front of the package. Copy and paste with your brain.',
        extendedDescription: 'If you don\'t have a package of gummy worms handy, this probably isn\'t the machine for you. Check out the `order()` machine in this pack.'
      }
    }
  },

  exits: {
    success: {
      outputFriendlyName: 'Protein (g)',
      outputDescription: 'The grams of gelatin-based protein in a 1kg serving.',
    },
    unrecognizedFlavors: {
      description: 'Could not recognize one or more of the provided `flavorStrings`.',
      extendedDescription: 'Some **markdown**.',
      moreInfoUrl: 'https://gummyworms.com/flavors',
    }
  },

  fn: function(inputs, exits) {
    // ...
    // your code here
    var result = 'foo';
    // ...
    // ...and when you're done:
    return exits.success(result);
  };
}

The top-level properties describe the machine itself. friendlyName is a display name in sentence-case, under 50 characters, with no ending punctuation. description is a one-sentence summary in the imperative mood ("Issue a JSON Web Token," not "Issues a JSON Web Token"), under 80 characters. Two optional fields provide depth: extendedDescription (under 2000 characters, full sentences allowed) and moreInfoUrl, which is useful for machines that wrap third-party APIs.

Two optional flags affect behavior. sideEffects can be set to cacheable or idempotent, and only machines without side effects should be marked cacheable — which enables the .cache() method. By default, machines are asynchronous; setting sync to true makes the machine a regular synchronous function.

Inputs

The inputs object declares what values the machine's function expects. Each input key is camel-cased, starts with a lowercase letter, and contains no special characters. Every input has its own friendlyName, description, and optional extendedDescription and moreInfoUrl. The example field defines the expected data type. Inputs are optional by default — if a value isn't provided, the fn receives undefined — so set required: true to make the machine throw an error for a missing value. For sensitive or hard-to-find inputs (API keys, tokens), the whereToGet object offers imperative-mood instructions on locating suitable values.

Exits

Exits define every possible outcome of the machine's function. The success exit is standardized and includes outputFriendlyName and outputDescription to describe the return value. Error exits follow the naming conventions of input keys. Each error exit has a description of when it triggers and can optionally include an extendedDescription with full Markdown support.

There's a learning curve here, but the conventions become second nature after authoring a single machine.

Machinepacks: What You Publish

On npm, you publish machinepacks — bundles of related machines for common development tasks. A machinepack working with arrays might include machines for concat(), map(), and similar operations. The naming convention is strict: every machinepack name must carry the machinepack- prefix followed by the descriptive name, like machinepack-array.

You can explore the Arrays machinepack in the registry to see a complete, working example.

Building a Machinepack

To illustrate authoring, we'll create a wrapper machinepack around the file-contributors npm package. Three tools get us started:

  1. Machinepack CLI: Install with npm install -g machinepack
    npm install -g machinepack
    
  2. Yeoman scaffolding tool: Install globally with
     npm install -g yo
    
  3. Machinepack Yeoman generator: Install with
    npm install -g generator-machinepack
    

Node.js and npm should already be installed. Note that the Yeoman generator has reported issues with Node.js 12 and 13 — for this workflow, use nvm to run Node.js 10.x, which is a confirmed working environment.

Generation and Exploration

Navigate to your target directory and run the generator:

yo machinepack

The interactive prompt scaffolds a barebones machinepack — answer yes to creating the example machine. The result produces these files:

DELETE_THIS_FILE.md
machines/
package.json
package.lock.json
README.md
index.js
node_modules/

With the Machinepack CLI installed, you can inspect and interact with any machinepack locally. To list available machines in the machines directory, run:

machinepack ls

This reveals the say-hello machine that ships with the generator. To see it in action, run:

machinepack exec say-hello

The CLI prompts for a name and prints the machine's output. Notice how the tool leverages standardization to derive description and functionality directly from the machine's metadata — no special handling needed.

Building Your Own Machine

To create a custom machine that wraps the file-contributors and node-fetch packages, first install those dependencies:

npm install file-contributors node-fetch --save

Next, generate a new machine using the CLI:

machinepack add

The CLI will prompt you for a friendly name, an optional description, and an optional extended description. Once you finish, the machine files are generated for you.

Open the generated machine in your editor and require the file-contributors package:

const fetch = require('node-fetch');
const getFileContributors = require('file-contributors').default;

global.fetch = fetch; // workaround since file-contributors uses windows.fetch() internally

Note: The node-fetch package and the global.fetch = fetch workaround are necessary because file-contributors calls windows.fetch() internally, which is unavailable in Node.js.

The getFileContributors function requires three arguments: owner (repository owner), repo (repository name), and path (file path). These belong in the inputs key:

...
 inputs: {
    owner: {
      friendlyName: 'Owner',
      description: 'The owner of the repository',
      required: true,
      example: 'DominusKelvin'
    },
    repo: {
      friendlyName: 'Repository',
      description: 'The Github repository',
      required: true,
      example: 'machinepack-filecontributors'
    },
    path: {
      friendlyName: 'Path',
      description: 'The relative path to the file',
      required: true,
      example: 'README.md'
    }
  },
...

For exits, the CLI creates a success exit by default. Modify that and add an error exit for failure cases:

exits: {

    success: {
      outputFriendlyName: 'File Contributors',
      outputDescription: 'An array of the contributors on a particular file',
      variableName: 'fileContributors',
      description: 'Done.',
    },

    error: {
      description: 'An error occurred trying to get file contributors'
    }

  },

Finally, implement the core logic in the fn function:

 fn: function(inputs, exits) {
    const contributors = getFileContributors(inputs.owner, inputs.repo, inputs.path)
    .then(contributors => {
      return exits.success(contributors)
    }).catch((error) => {
      return exits.error(error)
    })
  },

You've built your first machine. Test it with the CLI:

machinepack exec get-file-contributors

You'll be prompted for owner, repo, and path in sequence. If everything works, the machine exits with success and returns an array of contributors for the specified file.

Consuming Machines in Code

In a real application you won't call machines through the CLI. Here's how to consume a machine from a machinepack programmatically:

    var FileContributors = require('machinepack-filecontributors');

// Fetch metadata about a repository on GitHub.
FileContributors.getFileContributors({
  owner: 'DominusKelvin',
  repo: 'vue-cli-plugin-chakra-ui',
   path: 'README.md' 
}).exec({
  // An unexpected error occurred.
  error: function (){
  },
  // OK.
  success: function (contributors){
    console.log('Got:\n', contributors);
  },
});

Wrapping Up

You now know the machine specification, have built a machine of your own, and understand how to invoke machines from code.

Resources

The source repository and the published npm package are available for reference.

Smashing Editorial