Back in 2012, the engineering team at Heroku created a set of best practices for developing and running web apps. That document, consisting of 12 important rules, became the 12 Factor App manifesto. It gained a lot of traction over the years, especially as microservices took off. Along with microservices came a wave of related practices and tools — git, DevOps, Docker, Configuration Management — that all reinforced these principles.
This post walks through each of the 12 factors and how they apply to Node.js apps on TIBCO Cloud Integration.
Codebase #
One codebase tracked in revision control, many deploys. Keeping your code in version control is table stakes, but it’s especially important for 12 Factor compliance. The idea: one app, one repository. Developers can work on it without worrying about breaking other code (yes, unit testing matters here). I personally prefer git-based systems like GitHub or Gogs. Shared code across services should live in its own repository and be treated as a dependency.
“So what about the deploys?” A deploy is a single running instance of the microservice. With TIBCO Cloud Integration, each push automatically creates a new instance and you can run multiple versions in the same or separate sandboxes.
Dependencies #
Explicitly declare and isolate dependencies. Most languages have a package manager that handles installing libraries at deploy time. Node.js has two main options: npm and yarn. Both work off package.json, so switching between them is possible. TIBCO Cloud Integration standardizes on npm.
One thing to be careful about: pin your dependency versions. While you can specify “at least version x.y.x”, it’s better to lock to a specific tested version. You don’t want to wake up to a new dependency version breaking your app.
Configuration #
Store config in the environment. Config here means anything likely to change between deploys. The Visual Studio Code extension for TIBCO Cloud Integration generates a .env file for this purpose. Don’t commit that file to version control though — ask yourself: “Could I put this in a public repo without leaking credentials?” Usually not. Instead, create a .env.example with all the keys and dummy values.
TIBCO Cloud Integration injects environment variables into the container at runtime. Using the VSCode plugin, you can add variables with the Add environment variable command. In your code, reference them with a fallback:
var dbuser = process.env.DB_USER || 'defaultvalue';Backing services #
Treat backing services as attached resources. A backing service is anything your app depends on — an Amazon S3 bucket, an Azure SQL Server, etc. “Attached resource” means you access it through a URL. This makes local testing much easier since you don’t need an entire ecosystem running just to test one microservice. TIBCO Cloud Integration supports deploying Mock apps for API calls, and there are plenty of stub frameworks for other resources.
The alternative is giving every developer their own full environment with all backing services. And if you’ve hardcoded a dependency on a specific MySQL database and it needs to be replaced… do you really want to work over the weekend to fix that?
Build, Release, Run #
Strictly separate build and run stages. The manifesto defines three stages:
- The build stage: turn your code into an executable
- The release stage: takes the executable and adds the config
- The run stage: takes the output from the release stage and runs it on the target environment
This separation is critical for CI/CD pipelines — your code should move through environments without changes (only the config differs). This is why containerized environments stress treating containers as immutable objects. With TIBCO Cloud Integration, Node.js apps get this for free. When you push your app, you can specify a properties file that injects values into the container (see config above).
Processes #
Execute the app as one or more stateless processes. There’s still debate about why statelessness matters, and honestly it probably traces back to how easy it was to stuff everything into a monolith. But the rule is clear: shared data (including persistent data) belongs in a backing service, not in the app itself. The reason is scalability — if your app holds state, it can’t scale horizontally without risking duplicate actions or failures. Most Node.js apps start a single process (npm start or node .), but developers still need to make sure the app itself is stateless.
Port Bindings or Data Isolation #
Depending on which version of the manifesto you’re reading, the seventh factor is either port bindings or data isolation (the latter from the NGINX team’s update). For port bindings, the original definition says it well:
The twelve-factor app is completely self-contained and does not rely on runtime injection of a webserver into the execution environment to create a web-facing service. The web app exports HTTP as a service by binding to a port, and listening to requests coming in on that port.
Data Isolation makes perfect sense too (and maybe should have been the 13th factor ;-)). Every microservice should own its data, and you should only access that data through the microservice’s API. Violating this creates tight coupling between services, which is never a good idea.
Concurrency #
Scale out via the process model. For microservices, this means you should be able to run more than one instance. Containerized deployments like TIBCO Cloud Integration give you this out of the box. That said, you can easily break this by using timers inside your processes — a timer means you can’t scale up without running duplicate work.
Disposability #
I’ve always liked the phrase “treat your containers like cattle, not like pets.” Disposability is exactly that. You should be able to kill a container and start a new one without impact, or scale up and down in response to demand, painlessly. This is another reason stateless services matter. TIBCO Cloud Integration gives you scaling with the push of a button or a simple command.
Dev/Prod parity #
Keep your environments as similar as possible. Not just to minimize config changes during deployment, but to make sure your app behaves the same in staging and production. TIBCO Cloud Integration helps here with multiple sandboxes that keep the runtime environment consistent. It doesn’t handle your backing services, but having the runtime sorted is a good start :)
Logs #
A good microservice does one thing well (kind of like Linux commands — ps, grep). In a microservice environment, treat your logs as streams and send them elsewhere, unless logging is literally your microservice’s job. Most languages have solid logging frameworks. With Node.js on TCI, there’s a special logger class that matches the rest of the TCI log format. As a best practice, don’t use console.log().
Admin processes #
Administrative tasks and management processes shouldn’t live in your app. Run them as one-off processes in a separate container or thread. Data migrations, for example, should be one-off commands, not part of your regular deployment.
As always let me know what you think by posting a reply here or at the TIBCO Community