ponyfoo.com

Mastering Modular JavaScript

Module thinking, principles, design patterns and best practices — Modular JavaScript Book Series
O’Reilly Media164 PagesISBN 978-1-4919-5568-0

Tackle two aspects of JavaScript development: modularity and ES6. With this practical guide, front-end and back-end Node.js developers alike will learn how to scale out JavaScript applications by breaking codebases into smaller modules. We’ll specifically cover features in ES6 from the vantage point of how we can leverage them to improve modularity in our programs. Learn how to manage complexity, how to drive it down, and how to write simpler code by building small modules.

If you’re a frontend developer or backend Node.js developer with a working knowledge of JavaScript, this book is for you. The book is ideal for semi-senior developers, senior developers, technical leaders, and software architects.

This book is part of the Modular JavaScript series.

🗞 Start with the book series launch announcement on Pony Foo
💳 Participate in the crowdfunding campaign on Indiegogo
🌩 Amplify the announcement on social media via Thunderclap
🐤 Share a message on Twitter or within your social circles
👏 Contribute to the source code repository on GitHub
🦄 Read the free HTML version of the book on Pony Foo
📓 Purchase the book from O’Reilly on Amazon

Chapter 6

Development Methodology and Philosophy

Even though most of us work on projects with source code that is not publicly available, we can all benefit from following open source best practices, many of which still apply in closed-source project development. Pretending all of our code is going to be open source results in better configuration and secret management, better documentation, better interfaces, and more maintainable codebases overall.

In this chapter, we’ll explore open source principles and look at ways to adapt a methodology and set of robustness principles known as The Twelve-Factor App (generally devised for backend development) to modern JavaScript application development, frontend and backend alike.1

Secure Configuration Management

When it comes to configuration secrets in closed-source projects, like API keys or HTTPS session decryption keys, it is common for them to be hardcoded in place. In open source projects, these are typically instead obtained through environment variables or encrypted configuration files that aren’t committed to version-control systems alongside our codebase.

In open source projects, this allows the developer to share the vast majority of their application without compromising the security of their production systems. While this might not be an immediate concern in closed-source environments, we need to consider that once a secret is committed to version control, it’s etched into our version history unless we force a rewrite of that history, scrubbing the secrets from existence. Even then, it cannot be guaranteed that a malicious actor hasn’t gained access to these secrets at some point before they were scrubbed from history. Therefore, a better solution to this problem is rotating the secrets that might be compromised, revoking access through the old secrets and starting to use new, uncompromised secrets.

Although this approach is effective, it can be time-consuming when we have several secrets under our belt. When our application is large enough, leaked secrets pose significant risk even when exposed for a short period of time. As such, it’s best to approach secrets with careful consideration by default, and avoid headaches later in the lifetime of a project.

The absolute least we could be doing is giving every secret a unique name and placing them in a JSON file. Any sensitive information or configurable values may qualify as a secret, and this might range from private signing keys used to sign certificates to port numbers or database connection strings:

{
  "PORT": 3000,
  "MONGO_URI": "mongodb://localhost/mjavascript",
  "SESSION_SECRET": "ditch-foot-husband-conqueror"
}

Instead of hardcoding these variables wherever they’re used, or even placing them in a constant at the beginning of the module, we centralize all sensitive information in a single file that can then be excluded from version control. Besides helping us share the secrets across modules, making updates easier, this approach encourages us to isolate information that we previously wouldn’t have considered sensitive, like the work factor used for salting passwords.

Another benefit of going down this road is that, because we have all environment configuration in a central store, we can point our application to a different secret store depending on whether we’re provisioning the application for production, staging, or one of the local development environments used by our developers.

Because we’re purposely excluding the secrets from source version control, we can take many approaches when sharing them, such as using environment variables, storing them in JSON files kept in an Amazon S3 bucket, or using an encrypted repository dedicated to our application secrets.

Using what’s commonly referred to as dot env files is an effective way of securely managing secrets in Node.js applications, and a module called nconf can aid us in setting these up. These files typically contain two types of data: secrets that mustn’t be shared outside execution environments, and configuration values that should be editable and that we don’t want to hardcode.

One concrete and effective way of accomplishing this in real-world environments is using several dot env files, each with a clearly defined purpose. In order of precedence:

  • .env.defaults.json can be used to define default values that aren’t necessarily overwritten across environments, such as the application listening port, the NODE_ENV variable, and configurable options you don’t want to hardcode into your application code. These default settings should be safe to check into source control.

  • .env.production.json, .env.staging.json, and others can be used for environment-specific settings, such as the various production connection strings for databases, cookie encoding secrets, API keys, and so on.

  • .env.json could be your local, machine-specific settings, useful for secrets or configuration changes that shouldn’t be shared with other team members.

Furthermore, you could also accept simple modifications to environment settings through environment variables, such as when executing PORT=3000 node app, which is convenient during development.

We can use the nconf npm package to handle reading and merging all of these sources of application settings with ease.

The following piece of code shows how you could configure nconf to do what we’ve just described: we import the nconf package, and declare configuration sources from highest priority to lowest priority, while nconf will do the merging (higher-priority settings will always take precedence). We then set the actual NODE_ENV environment variable, because libraries rely on this property to decide whether to instrument or optimize their output:

// env
import nconf from 'nconf'

nconf.env()
nconf.file('environment', `.env.${ nodeEnv() }.json`)
nconf.file('machine', '.env.json')
nconf.file('defaults', '.env.defaults.json')

process.env.NODE_ENV = nodeEnv() // consistency

function nodeEnv() {
  return accessor('NODE_ENV')
}

function accessor(key) {
  return nconf.get(key)
}

export default accessor

The module also exposes an interface through which we can consume these application settings by making a function call such as env('PORT'). Whenever we need to access one of the configuration settings, we can import env.js and ask for the computed value of the relevant setting, and nconf takes care of the bulk of figuring out which settings take precedence over what, and what the value should be for the current environment:

import env from './env'

const port = env('PORT')

Assuming we have an .env.defaults.json that looks like the following, we could pass in the NODE_ENV flag when starting our staging, test, or production application and get the proper environment settings back, helping us simplify the process of loading up an environment:

{
  "NODE_ENV": "development"
}

We usually find ourselves needing to replicate this sort of logic in the client side. Naturally, we can’t share server-side secrets in the client side, as that’d leak our secrets to anyone snooping through our JavaScript files in the browser. Still, we might want to be able to access a few environment settings such as the NODE_ENV, our application’s domain or port, Google Analytics tracking ID, and similarly safe-to-advertise configuration details.

When it comes to the browser, we could use the exact same files and environment variables, but include a dedicated browser-specific object field, like so:

{
  "NODE_ENV": "development",
  "BROWSER_ENV": {
    "MIXPANEL_API_KEY": "some-api-key",
    "GOOGLE_MAPS_API_KEY": "another-api-key"
  }
}

Then, we could write a tiny script like the following to print all of those settings:

// print-browser-env
import env from './env'
const browserEnv = env('BROWSER_ENV')
const prettyJson = JSON.stringify(browserEnv, null, 2)
console.log(prettyJson)

Naturally, we don’t want to mix server-side settings with browser settings. Browser settings are usually accessible to anyone with a user agent, the ability to visit our website, and basic programming skills, meaning we would do well not to bundle highly sensitive secrets with our client-side applications. To resolve the issue, we can have a build step that prints the settings for the appropriate environment to an .env.browser.json file, and then use only that file on the client-side.

We could incorporate this encapsulation into our build process, adding the following command-line call:

node print-browser-env > browser/.env.browser.json

Note that in order for this pattern to work properly, we need to know the environment we’re building for at the time that we compile the browser dot env file. Passing in a different NODE_ENV environment variable would produce different results, depending on our target environment.

By compiling client-side configuration settings in this way, we avoid leaking server-side configuration secrets onto the client-side.

Furthermore, we should replicate the env file from the server side to the client side, so that application settings are consumed in much the same way on both sides of the wire:

// browser/env
import env from './env.browser.json'

export default function accessor(key) {
  if (typeof key !== 'string') {
    return env
  }
  return key in env ? env[key] : null
}

There are many other ways of storing our application settings, each with its own associated pros and cons. The approach we just discussed, though, is relatively easy to implement and solid enough to get started. As an upgrade, you might want to look into using AWS Secrets Manager. That way, you’d have a single secret to take care of in team members’ environments, instead of every single secret.

A secret service also takes care of encryption, secure storage, and secret rotation (useful in the case of a data breach), among other advanced features.

1
You can find the original Twelve-Factor App methodology and its documentation online.
2
When we run npm install, npm also executes a rebuild step after npm install ends. The rebuild step recompiles native binaries, building different assets depending on the execution environment and the local machine’s operating system.
Unlock with one Tweet!
Grants you full online access to Mastering Modular JavaScript!
You can also read the book on the public git repository, but it won’t be as pretty! 😅