How to deprecate features in your API before making breaking changes

When creating APIs that external parties depend on you will soon realize the need to be careful about making breaking changes to that API as even a single renamed property/function might cause existing code using your API to fail.

As semver has grown in popularity and many npm packages have adopted this way of versioning there is also a need to handle breaking changes in a good way. If you are not familiar with the concept I would highly recommend that you read some about it on their site.

Deprecation is a technique of marking which parts of your API will soon be subject to change. To treat your API users nicely it also good to make sure to keep both your deprecated and new API working in parallel for a while so that your users have time to migrate their existing code bases.

The time to wait before finally removing your deprecated API will depend on how often you are making releases and how much maintaining your deprecated API functionality costs in maintenance time, but usually, it's good to keep both versions working during two major releases. That way if you include any new deprecations in the changelog for your next release the users will have time to adapt to the upcoming changes.

As for how to inform your API users about deprecations and suggestions of alternative newer features that can replace the deprecated I have some suggestions as seen in the example below with the help of a simple function that will warn the user of your API by producing a log in case a deprecated feature is being used:

class ALegacyFeature {
  constructor() {
    deprecated(`The use of ${this.constructor.name} is deprecated, please use class ARecentFeature instead.`);
  }
}

function parser() {
  deprecated(`The use of ${this.name} is deprecated, please use the function betterParser instead.`);
}

By informing the user and helping out with alternative suggestions, the effort needed to make the update is greatly reduced. Also, it will keep reminding the user until they have adopted the newer feature instead.

Also, we would want to make sure that such a function would only log once per time the application is run to prevent spamming the logs.

This is my suggested implementation:

const alreadyLogged = new Set();

export function deprecated(message) {
  const log = `deprecated: ${message}`;
  const logHash = hashString(log);

  if (alreadyLogged.has(logHash) === false) {
    console.warn(log);
    alreadyLogged.add(logHash);
  }
}

function hashString(value = '') {
  // Code from https://stackoverflow.com/a/7616484/231582
  let result = 0;

  for (let i = 0; i < value.length; i++) {
    result = ((result << 5) - result) + value.charCodeAt(i); // eslint-disable-line no-bitwise
    // Convert to 32bit integer
    result |= 0; // eslint-disable-line no-bitwise
  }

  return result;
}

We simply keep a Set of message hashes for messages that have already been logged to the user and then log a warning with the prefix "deprecated:" for consistency.

This maybe goes without saying, but please do make sure that these messages are logged in a way so that they would not display in the UIs of any apps using your API as these messages are for the developers of those apps and not the end-users.

As always this works better the smaller your API surface is. Modules with a smaller API surface will almost always be easier to learn and work with. Put all the smart stuff inside your modules and only expose the bare minimum to make it useful for your users.

Happy coding!