Don't version APIs, version resources. I.e., this is wrong:

https://example.com/api/v1/resource

Global version number has a few problems:

May be there's more, but the first problem is quite enough, if you ask me. And I can't think of a single disadvantage of versioning resources independently.

Versioning resources

Technically, you can do this:

Content-type: application/myformat+json; version=2.0

… or this:

{
  "version": "2.0",
  "payload": { ... }
}

… or even this:

https://example.com/api/resource?version=2.0

It doesn't matter. Since we don't live in the pure-REST utopia predominantly using well-defined MIME types, your representation formats are probably custom anyway, so your versioning is going to be custom as well. As long as it's documented, it's fine.

Implicit versioning

Instead of giving your format an explicit version number, you can change data in a way that the old clients could not use it. For example, if you've subtly changed the format of a date field, change the name of the field too:

Version 1:

{"time": "2012-01-01T00:00"}

Version 2:

{"utctime": "2012-01-01T08:00"}

You could even support both fields for some time with a deprecation warning in the docs.

Even though I like this approach more than numbering, I'm not going to defend it to the death. I think it boils down to the kind of developers you want to cater to (including your own). Some people believe that everything should be declared in advance, preferably with a formal schema. But schemas are harder to maintain. Other people rely on catching exceptions at run-time and introspection. But run-time sometimes means "production" and bad things might happen more often than you'd like.

Comments: 4

  1. Alexander Batishchev

    I'd prefer (X-)ApiVersion header instead.

  2. Ivan Sagalaev

    I'd prefer (X-)ApiVersion header instead.

    As long as it's per-resource, not global through the whole API — it's okay.

    The exact version declaration is not the issue. The issue is: does your API allows more granular changes than "all at once".

  3. Andrei U.

    Общее замечание: слишком абстрактный текст и примеры. Без конкретных примеров очень сложно рассуждать о плюсах и минусах того или иного подхода.

    Несколько комментариев по тексту:

    A backwards incompatible change to any one resource invalidates all clients, even those who don't use this particular resource. This is unnecessary maintenance burden on client developers.

    Одна из задач версионирования API как раз не ломать существующих клиентов, изолировав таким образом несовместимые изминения.

    Появление новой версии API совсем не означает немедленную необходимость миграции на него. В определенных случаях обновление клиентской части вообще мало возможно (например, прошивки устройств) и старый API должен существовать пока такие клиенты находятся в использовании. Что в этом случае подразумевается под "unnecessary maintenance burden on client developers"?

    It constrains development on the server as you have to synchronize releases of independent backwards incompatible features to fit them into one version change.

    Опять же хотелось бы более конкретный пример и контекст. При глобальном версионировании каждая конкретная версия API может быть имплементирована как независимый сервис со своим стеком. С точки зрения разработки они могут не иметь вообще никакого общего кода, что в таком случае нужно синхронизировать?

    You have to maintain several copies at least to versions of your whole API code base simultaneously.

    Речь идет именно о разработке или эксплуатации? Если речь о разработке, то старая версия API вообще может быть заморожена и коммиты туда происходят только для критических фиксов. Бренчи и теги созданны в т.ч. для этого.

    VERSIONING RESOURCES/IMPLICIT VERSIONING

    Такой подход как раз усложняет изоляцию изменений. В том случае, если разные версии API имплементированы как независимые сервисы, делать роутинг между ними становится сложнее, особенно если версия указывается не в заголовках или url, а в теле сообщения.

    Введение же новых имен полей резко снижает общую прозрачность системы. Как например понять, к какой версии API был сделан запрос без заглядывания в тело сообщения?

    Что делать в том случае, если клиент не только читает данные, но и модифицирует их. Как сервер будет различать сообщения различных версий в случае implicit versioning? Вычисление наличия сабсетов различных полей не выглядит очень элегантным решением.

    На этом пока все :). Не уверен, что понял правильно то, что вы пытались сказать. С конкретными примерами обсуждать плюсы и минусы различных подходом было бы значительно проще.

  4. Ivan Sagalaev

    Андрей, спасибо за равёрнутый вопрос. Про необходимость поддержки кода и синхронизацию изменений я писал в продолжении. А про ситуацию с обязательным сохранением работы клиентов написал ещё одно.

    Немножко добавлю про неявное версионирование.

    Да, в отличие от явного версионирования это не универсальный подход, я лишь заметил, что так иногда можно, и иногда проще. Другими словами: если ваше обратно несовместимое изменение маленькое и локальное, то имеет смысл превратить его в обратно совместимое, просто добавив поле в другом формате, вместо того, чтобы удалять старое и менять версию.

    Что делать в том случае, если клиент не только читает данные, но и модифицирует их. Как сервер будет различать сообщения различных версий в случае implicit versioning?

    Неважно, чтение или модификация. В примере с полем времени сервер будет делать что-то такое:

    time = data.haskey('utctime') ? fromutc(data['utctime']) : fromlocal(data['time'])
    

Add comment