Node.js JavaScript Programming

I keep my JavaScript in a library of small modules, each a folder that any project can load with require('Name'). The folder they live in isn't an npm one: it's a node_modules folder at the top of the code tree, and Node's usual lookup finds it from any project underneath. Every module follows the same few rules, which make the code easy to find your way around and easy to search. Let's build a small one.

One tree per module

A module exports a single object, and that object is the whole of the module: every function and type it has sits somewhere in its tree. Here's a small Game module:

Game
├── Player
│   ├── Create
│   └── Rename
└── Score
    ├── Add
    └── Format
The tree of names in a small Game module

Everything is called by its path through the tree: Game.Player.Create('Bob'), Game.Score.Add(score, 10). There are no classes to construct and nothing else to import. You load the module and walk down to what you need.

Files follow the tree

Each branch of the tree gets a file, named after its full path, inside a folder named after the branch above it:

Game/
  index.js
  src/
    Game.Player.js
    Game.Score.js
    Player/
      Game.Player.Create.js
      Game.Player.Rename.js
    Score/
      Game.Score.Add.js
      Game.Score.Format.js
The Game module's files, one per branch

Because a file is named after its full path, there's never any doubt about which file holds Game.Player.Create, and no two files in the whole library share a name.

Building the tree with require

The tree is put together with require. index.js holds the top of it, with a require for each branch:

const Game =
{
    Player: require('./src/Game.Player'),
    Score: require('./src/Game.Score'),
}

module.exports = Game
index.js, the top of the Game module's tree

A branch file does the same one level down. It writes out the tree from the root to its own branch, fills in only that branch, and exports just that part:

const Game =
{
    Player:
    {
        Create: require('./Player/Game.Player.Create'),
        Rename: require('./Player/Game.Player.Rename'),
    }
}

module.exports = Game.Player
src/Game.Player.js, which builds the Player branch

And at the leaves are the functions themselves, each in the same shape, with the whole path written out above it:

const Security = require('Security')

const Game =
{
    Player:
    {
        Create: (name) => {
            Security.String(name)

            return {
                _is: 'Game.Player',
                name,
                score: 0,
            }
        }
    }
}

module.exports = Game.Player.Create
src/Player/Game.Player.Create.js, one function in its place in the tree

Every file looks like a small piece of the same object. Open any of them and the first thing you see is exactly where in the module you are.

Using the rest of the module

When a function needs another part of its own module, the file requires it into the same place in its own copy of the tree. Then the call inside the file is written exactly as it's written everywhere else:

const Security = require('Security')

const Game =
{
    Player:
    {
        Create: require('./Game.Player.Create'),

        Rename: (player, name) => {
            Security.Object('Game.Player')(player)
            Security.String(name)

            const renamed = Game.Player.Create(name)
            renamed.score = player.score
            return renamed
        }
    }
}

module.exports = Game.Player.Rename
src/Player/Game.Player.Rename.js, using Game.Player.Create from the same module

Other modules

Other modules are required at the top of the file, one line each, by their bare name, into a variable with exactly that name:

const Security = require('Security')
const Collection = require('Collection')
Requiring two other modules by name

The list at the top of a file is then a complete list of what it depends on.

Full names everywhere

This is the rule that matters most: anything is always referred to by its whole dotted name, inside its own module and in every other. Nothing is copied into a variable with a shorter name, nothing is destructured, nothing is renamed:

//  Not like this
const { Create } = require('Game').Player
const player = Create('Bob')

//  Like this
const Game = require('Game')
const player = Game.Player.Create('Bob')
The same call written with a shortened name, and with the full name

The name in the call is the name in the definition is the name of the file. So a search of the whole code base for Game.Player.Create finds the file that defines it and every single place it's used, and nothing else. Renaming something is a search and replace. And reading any line, you know where the function it calls lives without looking anything up.

Nothing runs on load

Requiring a module builds its tree of functions and does nothing else. No timers start, no files are read, no connections are opened and no state is set up when a file is included. Work only happens when you call a function.

That makes requiring anything safe: tests, tools and scripts can load any module without side effects, and the order modules are loaded in never matters. Where a module does keep state, it sits in its tree under a name starting with an underscore, such as Security._stack, which marks it as the module's own business.

Checking every argument

Every function starts by checking each of its arguments with the Security module, one line per argument, before it does anything else. You can see it in Game.Player.Create and Game.Player.Rename above. The checks catch a bad value in the function it was passed to, and the first few lines of any function say what it takes.

Naming

  • Modules, branches and functions are capitalised words: Game, Player, Create.
  • Files are the full dotted name with .js on the end, in a folder named after the branch above.
  • Arguments and local variables are camel case, and an argument that can be left out starts with optional: optionalCharacter, optionalCallback.
  • Typed objects carry their full type name in an _is field, so Game.Player.Create makes a 'Game.Player'.
  • Names starting with an underscore are for the module's own use.

With these rules, a module's list of files is also a table of contents for it.

See also