A Better Versioning Technique for Frontend Applications
The healthy approach

A little bit of story how did we got here.
We’ve all been there at some point in time. App versions are simplified to just whole numbers and increment with every release… or not. The randomness and lack of rules didn’t help when working in a larger organisation and where multiple teams may get involved in a single codebase. Fortunately, it’s pretty easy to fix the problem when people actually care.
Back in 2016, after I finished building a new architecture for Yell.com, I started wondering what would be the best way to version our app. How to reflect the release seriousness with just a single number. The idea came from my former boss, Steve Workman, to try SemVer 2.0. It was an instant buy-in from the team, although nobody wanted to manage it manually. So we put in place build process scripts to automate versions based on our needs and push to correct repositories to avoid collisions and mistakes. Our new approach worked well for years and helped us develop a better git branching strategy, release notes management, and robust deployment pipelines.
I’m always trying to avoid setting strict rules for an engineering team, but this one is an exception. It’s crucial to understand what we can expect from the code without digging through the repo like a pig on a beach looking for truffles. Creating a solid app versioning approach is vital in smoothing the waters.
Our new approach worked well for years and helped us develop a better git branching strategy, release notes management, and robust deployment pipelines.
By the way, you can read about the SimGit Flow branching strategy in my post here.
What is SemVer 2.0?
A simple set of rules and requirements explains how version numbers are assigned and incremented. These rules are based on but not necessarily limited to pre-existing widespread standard practices in both closed and open-source software. They are called “Semantic Versioning.” Under this scheme, version numbers and the way they change convey meaning about the underlying code and what has been modified from one version to the next.
Consider a version format of X.Y.Z (Major.Minor.Patch
). Bug fixes not affecting the API increment the patch version, backwards compatible additions/changes increment the minor version, and backwards-incompatible changes increment the major version.
Read more about Semantic Versioning here:
It was designed with APIs in mind but works very well with frontend apps, especially when dealing with multiple apps in a mono repo and sharing a bunch of functionality (Core) between them. SimGit Flow branching strategy utilizes this versioning for branch naming; thus, it is a perfect match if you use the SemVer approach as you can trust backward compatibility based only on the version number.
How does that work?
Given a version number MAJOR.MINOR.PATCH
, increment the:
- MAJOR version when you make incompatible changes.
This would include significant redesigns and things that somehow “start a new era”, as I call it. For example, throughout 4 years at Yell, we only reached version 9.x.x. - MINOR version when you add backward functionality.
These would include typical releases with new features and functionality added to the site. Sometimes these updates might be not backward compatible for the given app from your mono repo, but that’s OK, as long as the Core (shared components) is compatible. In some cases, we may use minor bumps for bug fixes, but it depends on the situation as we do not always we wanted to use the hotfix approach. - PATCH version when you make backward-compatible bug fixes.
When a new build is prepared as x.x.0, we do testing, find bugs, fix and deploy again. Every iteration and fixes increment the patch version. A new release almost always requires some minor fixes after the QA verification. Patch bump will also be used almost always when we need to do a hotfix to a live code.
How do we use SemVer in our apps?
- We store the version in
package.json
file. It’s a logical choice for such information, and the build process can easily access it. - We use the version number for cache-busting. All our build files and assets have suffixes with the app version, so we don’t have to worry about caching issues whenever a new code is deployed. Also, it’s human-readable to quickly identify the source of problems and deployed versions.
- We used
MAJOR.MINOR
to name our release branches. It helps to quickly find the code and rollout hotfixes out of specific versions without affecting other branches. - All 3rd party analytics and performance tools receive the version number in the dataset so we can quickly identify anomalies and issues, and pinpoint them to a specific version.
- After each build has been created, we zip all files into one package and use the app version as a filename. Just makes things easier (again) to find later.
- We are looking to create release environments based on versions and destroy them as soon as the code goes live. This will allow us to have more than one release in progress at a given time.
- … and more
All version updates are handled by the build process, and when we run a new one, we also define what type of bump we want — major, minor, or patch.
On Major or minor build — the script creates a new release branch to merge all code there. After it is made, any update to this particular release is a “patch build”.
On patch build —we need to provide version number (major.minor
), and the script will pull in this particular branch, bump the patch version, build a package, and deploy.
Once everything is tested, and we are happy, we can push live. It’s all part of our automated pipelines, which stitch together the SimGit Flow branching strategy I mentioned earlier, SemVer versioning and release notes flow (I will cover in yet another post soon).
Example script to deal with version updates
This is an example of our build process and how we deal with it.
A few things you will likely need:
/**
* Install semver NPM package to deal with versioning
*/
npm install -D semver
npm install -D shelljs // optional
And the script to help you handle the updates:
/**
* Check what build you are preparing as you will need to pass
* the name to semver script to update to the new version.
*/
const ALLOWED = {
patch: true,
minor: true,
major: true
};if (!ALLOWED[process.argv.slice(2)[0]]) {
console.log('\x1b[31m* Wrong version type!\x1b[0m');
shell.exit();
} else {
buildType = process.argv.slice(2)[0];
}/**
* Run version update.
* Read package.json, update version based on buildType, save back.
*/
let rawdata = fs.readFileSync('package.json');
let packageJson = JSON.parse(rawdata);
console.log('* Current version is', packageJson.version);
packageJson.version = semver.inc(packageJson.version, buildType);
console.log('* New version is', packageJson.version);
// save package json
let data = JSON.stringify(packageJson);
fs.writeFileSync('package.json', data);/**
* Make it pretty again ;)
*/
shell.exec('npx prettier --write package.json');
The good thing is that the semver
npm package will take care of all the logic required to update your version name correctly. All you have to do is read the current version, provide it to semver
and say what you want to do with it.
We are using shelljs
to execute shell functions from our npm scripts. You will see this in the code above as shell.exec()
.

Last few words…
It’s not rocket science, but I often saw chaos in app versioning and engineering teams following a loose or random approach without a formal agreement. Whatever you decide to do, make sure it is written down, explained, and implemented by all devs. The best process will fail if it’s not followed by teams.
Consider SemVer
as a great place to start and an early warning signal, a traffic light for your code. It will give your team a clear indication of when version change is tiny and when it’s severe and help save you from many problems during deployments. A major release is a big thing that usually requires syncing multiple teams, and everybody knows to watch out when they are ready to upgrade. Minor is a regular thing, regular releases. The patch is just another iteration and nothing to worry about.
Works like a treat and it’s easy to understand and follow.