~semantic versioning

August 10, 2019

for those familiar with software development, the concept of versioning should be a core practice. however, there may still be some developers who haven’t fully embraced its importance or are new to the wonders of semantic versioning (semver). this guide is designed for those looking to understand the mechanics and best practices behind semver, a widely adopted versioning system. while semver is the most prevalent, it's not the only method. alternatives like calendar versioning (calver) and project codenames exist, but semver's precision and clarity make it a go-to choice in the software world.

if you're working on a software library or project intended for public use, following a standardized versioning scheme isn't optional—it’s a requirement. versioning communicates changes to users and other developers in a predictable, structured way. now, let’s dive into the technical specifics of semver and how it’s implemented.

the semver breakdown

in semantic versioning, each release is labeled using a three-part numbering scheme: major.minor.patch. here’s a typical example:

1.66.2

  • 1 is the major version.
  • 66 is the minor version.
  • 2 is the patch version.

each part of this numbering scheme carries important information about the scope and impact of the changes introduced. let’s explore each in detail:

major version: breaking changes and api overhauls

the major version denotes breaking changes in the public api that are not backward-compatible. any increment in the major version means that users will need to adapt their code, as prior versions will no longer function as expected. for instance, when next.js released version 10, significant changes occurred in the way dynamic routes and api routes were handled:

  1. image optimization: next.js 10 introduced a built-in image component for automatic image optimization, allowing for on-demand image resizing and improved performance, a feature not present in previous versions.
  2. internationalized routing: next.js 10 also introduced support for internationalized (i18n) routing, completely changing how routes were handled for multiple languages in a single next.js application.

these are the types of changes that demand a major version bump. the goal here is to signal to developers that upgrading to this version might require changes in their application. care should be taken when increasing the major version, as frequent breaking changes can alienate your user base by requiring them to continually refactor their code.

for example, a content management system built with next.js might bump from version 1.x.x to 2.x.x if they remove ssr (server-side rendering) in favor of full static generation, fundamentally changing the way the app delivers content.

minor version: backward-compatible enhancements

a minor version increment introduces new features or significant improvements while maintaining full backward compatibility. no existing functionality is broken, and users can safely upgrade without fear of regression.

for example, next.js 9.3 introduced support for incremental static regeneration (isr). this feature allowed developers to update static pages after a build, creating more dynamic static sites without needing to perform full rebuilds. while isr was a significant new feature, it didn’t disrupt the previous methods of generating static content, making it a perfect example of a minor version update.

minor versions can also mark deprecated apis, warning developers of upcoming changes. for instance, when an api becomes obsolete but is still functional, incrementing the minor version signals that developers should start transitioning away from the deprecated api. when this happens, the patch number resets to 0.

patch version: bug fixes and optimizations

the patch version is reserved for bug fixes that don’t affect the public api. these changes typically address issues that users may not even notice unless they were experiencing specific bugs. it’s a lightweight version update that resolves defects without introducing new features.

for example, suppose you discover a bug in a custom hook within your next.js application that causes state to not properly persist between page navigations. fixing this issue without altering the public-facing api would result in a patch version increment, e.g., from 1.2.0 to 1.2.1.

it’s critical to keep patch versions isolated to bug fixes. any improvements to performance or minor api tweaks should typically fall under a minor version update unless they have no functional impact.

a practical example

let’s walk through a hypothetical scenario of a next.js-based project:

  • initial release (0.1.0): you build an mvp for a blog platform using next.js, which supports static generation for posts and server-side rendering (ssr) for the admin dashboard. since it's not feature-complete, the version is pre-1.0.0, signaling that breaking changes are still possible.

  • new feature (0.2.0): you add support for incremental static regeneration (isr) to improve performance and allow posts to be updated in the background. this feature does not disrupt existing static or server-rendered content, so the minor version is bumped to 0.2.0.

  • bug fix (0.2.1): you discover a bug in the pagination system that caused incorrect results when users filtered posts by tags. fixing this issue without modifying the api results in a patch version increment (0.2.1).

  • stability milestone (1.0.0): after adding more features like draft support and securing the platform, the blog platform is deemed stable and feature-complete. version 1.0.0 is released, signaling that the project is ready for production use.

pre-release versions and build metadata

occasionally, you may encounter versions with additional tags such as 1.0.0-alpha.2 or 1.0.0-beta.1. these are pre-release versions, denoting work-in-progress or experimental features. the hierarchy is generally:

  • alpha: early testing, unstable.
  • beta: feature-complete but may still contain bugs.
  • release candidate (rc): near-final version, pending further testing.

build metadata (e.g., +build.f862f5d) refers to specific identifiers for the build process, typically used internally for ci/cd pipelines or version tracking.

managing dependencies

when managing external dependencies, it's important to respect their versioning system:

  • major version upgrade: if a dependency introduces breaking changes, evaluate whether it affects your public api. if it does, you may need to release a major version of your own library.
  • minor version upgrade: if the changes introduce new features that you want to expose, a minor version bump may be appropriate.
  • patch version upgrade: bug fixes in dependencies that impact your library’s functionality should be reflected with a patch version bump.

closing thoughts

versioning is more than just numbers—it’s a strategy. by adopting semver, developers can maintain a predictable, organized, and professional release process. moreover, creating release notes or changelogs is essential, as it provides both developers and users with a transparent view of the history of changes.

versioning is not only about features and bug fixes but also plays a key role in regression testing, a/b testing, and release management strategies. as a software engineer, mastering semver helps ensure the reliability and consistency of your code.

references