The 12-factor app is an extremely useful set of guidelines for avoiding systemic problems in server-side web application development, written by the folks at Heroku. Asking whether or not something is 12-factor compliant can be a quick way to cut through the noise and make a probably-pretty-good decision. For example: if you're stuck philosophizing over whether your Rails app should write logs to a file vs
stdout, the section on logs offers a clear suggestion for what to do and why.
But I've noticed a tendency to over-generalize the 12-factor guidelines, dogmatically prescribing these "rules" in contexts where they don't necessarily apply or even make sense – specifically, in client-side applications like single-page apps. Principles and best practices are great, but thinking critically about their underlying goals and modifying them (or throwing them out entirely!) as needed to fit particular situations is even better.
The 12-factor guidelines are written for apps that run on a server, not in a browser. Still, several of the 12 guidelines can be fruitfully applied beyond server-side apps, especially if you're down to bend them out of shape a little. Here is my breakdown of which of the 12 factors are relevant to SPAs and which aren't – to be taken with 12 grains of salt:
- Codebase: one codebase tracked in revision control, many deploys
✅ Amazing. Keep it! (but to avoid bikeshedding can we please tweak it to also be fine with monorepos?)
- Dependencies: explicitly declare and isolate dependencies
✅ Yes please! Use JS modules or whatever makes sense in the language you're using.
- Config: store config in the environment
⚠️ There are two useful goals to extract from this section: A) strict separation of config from code, and B) granular config. Separating config from your compiled JS bundle means producing fewer bundles that are more flexible and portable across deploys (e.g. dev -> staging -> prod), which means less waiting around on webpack minifying, CSSifying, and doing whatever else it does. Granular config (as opposed to grouped config items) creates additional flexibility, for example, making it so you can deploy a version of your app that talks to a non-production version of the
DOGS_API, but a production version of the
CATS_API, without having to recompile a bundle. How to ship the same JS bundle across N environments includes step-by-step instructions for an implementation of these two goals.
Trying to apply anything more from this 12-factor guideline to SPAs leads to a giant mess, because:
- The core recommendation is impossible – we can't store config in environment variables because those don't exist in the browser.
- "Config files" are given the thumbs-down, but in the browser, if we were to split config into a separate JS file, you might call it a config file, but that's not the kind of config file they're talking about.
- Incidentally many SPA projects use environment variables at build-time to bake configuration into the bundle, so isn't that "storing config in the environment"? It is not, that is storing config in the build environment!
TLDR: there are some useful ideas here but they need serious adaptation
- Backing services: treat backing services as attached resources
⚠️ ️️The goal of loosely coupling deployments and the services the SPA consumes over the network translates decently and can be achieved in the SPA context by, for example, putting backend service URLs into config rather than the compiled JS bundle. But the big idea of treating local and remote backing services identically is pretty much lost in translation.
- Build, release, run: strictly separate build and run stages
✅ Love it
- Processes: execute the app as one or more stateless processes
⛔ ️Makes no sense – our app is executing inside the isolated context of another app (the browser) that is executed as a process
- Port binding: export services via port binding
⛔ Makes no sense – we aren't exporting web services and we can't listen on ports
- Concurrency: scale out via the process model
⛔ Makes no sense – builds on top of #6
- Disposability: maximize robustness with fast startup and graceful shutdown
⛔ Makes no sense – but do try to minimize startup time and make apps resilient to network unreliability, etc.
- Dev/prod parity: keep development, staging, and production as similar as possible
✅ Who doesn't love this???
- Logs: treat logs as event streams
⛔ Makes no sense – among other things, there is no
- Admin processes: run admin/management tasks as one-off processes
⛔ Makes no sense – the kind of admin tasks described are server-specific
By my count four are directly applicable, six are basically irrelevant, and the remaining two contain useful advice that needs adaptation. Let me know if your count is different!
I think we would probably be better off coming up with a separate set of guidelines for single-page apps that incorporates the useful things from and pays homage to the 12-factor app, because that would be simpler to communicate. If you're interested in this kind of SPA-specific set of guidelines, you may enjoy Immutable Web Apps.