{
    "archive_path": "archive/1780117164.121718",
    "base_url": "mnot.net/blog/2012/api-evolution",
    "basename": "api-evolution",
    "bookmarked_date": "2026-05-30 04:59",
    "canonical": {
        "archive_org_path": "https://web.archive.org/web/mnot.net/blog/2012/api-evolution",
        "dom_path": "output.html",
        "favicon_path": "favicon.ico",
        "git_path": "git/",
        "google_favicon_path": "https://www.google.com/s2/favicons?domain=mnot.net",
        "headers_path": "headers.json",
        "htmltotext_path": "htmltotext.txt",
        "index_path": "index.html",
        "media_path": "media/",
        "mercury_path": "mercury/content.html",
        "pdf_path": "output.pdf",
        "readability_path": "readability/content.html",
        "screenshot_path": "screenshot.png",
        "singlefile_path": "singlefile.html",
        "warc_path": "warc/",
        "wget_path": null
    },
    "domain": "mnot.net",
    "downloaded_at": "2026-05-30T04:59:28.826589+00:00",
    "downloaded_datestr": "2026-05-30 04:59",
    "extension": "",
    "hash": "D72AC7TE0HR9281T99PJ",
    "history": {
        "archive_org": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--head",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://web.archive.org/save/https://mnot.net/blog/2012/api-evolution"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-05-30T05:00:57.126669+00:00",
                "index_texts": null,
                "output": "api-evolution.html.gzip",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T05:00:30.286146+00:00",
                "status": "succeeded"
            }
        ],
        "dom": [
            {
                "cmd": [
                    "/usr/bin/chromium-browser",
                    "--proxy-server=socks5://tor-socks-proxy:9150",
                    "--disable-features=DarkMode",
                    "--run-all-compositor-stages-before-draw",
                    "--hide-scrollbars",
                    "--autoplay-policy=no-user-gesture-required",
                    "--no-first-run",
                    "--use-fake-ui-for-media-stream",
                    "--use-fake-device-for-media-stream",
                    "--simulate-outdated-no-au='Tue, 31 Dec 2099 23:59:59 GMT'",
                    "--headless=new",
                    "--no-sandbox",
                    "--no-zygote",
                    "--disable-dev-shm-usage",
                    "--disable-software-rasterizer",
                    "--disable-sync",
                    "--window-size=1440,2000",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "--user-data-dir=/data/personas/Default/chrome_profile",
                    "--profile-directory=Default",
                    "--dump-dom",
                    "https://mnot.net/blog/2012/api-evolution"
                ],
                "cmd_version": "131.0.6778",
                "end_ts": "2026-05-30T04:59:48.670739+00:00",
                "index_texts": null,
                "output": "output.html",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T04:59:40.388716+00:00",
                "status": "succeeded"
            }
        ],
        "favicon": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--max-time",
                    "60",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://www.google.com/s2/favicons?domain=mnot.net"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-05-30T04:59:32.471932+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T04:59:29.167117+00:00",
                "status": "succeeded"
            }
        ],
        "git": [],
        "headers": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--head",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://mnot.net/blog/2012/api-evolution"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-05-30T04:59:32.634913+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T04:59:32.571010+00:00",
                "status": "succeeded"
            }
        ],
        "htmltotext": [
            {
                "cmd": [
                    "(internal) archivebox.extractors.htmltotext",
                    "./{singlefile,dom}.html"
                ],
                "cmd_version": "0.8.5rc51",
                "end_ts": "2026-05-30T05:00:20.880430+00:00",
                "index_texts": [
                    "Evolving HTTP APIs (/style) (/blog/) (Home) (https://www.mnot.net/blog/index.atom) (Mark Nottingham) (https://www.mnot.net/blog/index.atom) (Mark Nottingham)  Mark Nottingham (/blog/) recent entries (/blog/all/) all entries (/blog/index.atom) (feed for this blog) feed  (/personal/) ()   Hi, I\u2019m Mark Nottingham.\nI write about the Web, protocol design, HTTP, Internet governance, and more. This is a personal\nblog, it does not represent anyone else. (/personal/) Find out more . Comments? Let's talk on Mastodon. (https://techpolicy.social/@mnot) @mnot@techpolicy.social   other HTTP APIs posts  (/blog/2018/header_compression) Designing Headers for HTTP Compression Tuesday, 27 November 2018 227 readers in ~2 weeks  (/blog/2017/status_codes) How to Think About HTTP Status Codes Thursday, 11 May 2017 226 readers in ~2 weeks  (/blog/2013/linking_apis) Five Reasons to Considering Linking in Your HTTP APIs Sunday, 23 June 2013 533 readers in ~2 weeks  (/blog/2013/http_problem) Indicating Problems in HTTP APIs Wednesday, 15 May 2013 134 readers in ~2 weeks  (/blog/2012/patch) Why PATCH is Good for Your HTTP API Wednesday,  5 September 2012 184 readers in ~2 weeks  (/blog/2012/header_versioning) Bad HTTP API Smells: Version Headers Wednesday, 11 July 2012 249 readers in ~2 weeks  (/blog/2012/http_api_complexity_model) HTTP API Complexity Monday, 25 June 2012 84 readers in ~2 weeks  (/blog/2011/linking_in_json) Linking in JSON Friday, 25 November 2011 440 readers in ~2 weeks  (/blog/2010/well-known) RFC 5785 - Well-Known URIs Wednesday,  7 April 2010 181 readers in ~2 weeks  (/blog/2007/squid) Squid is My Service Bus Sunday, 29 April 2007 124 readers in ~2 weeks  (/blog/2006/extensibility) Are Namespaces (and mU) Necessary? Friday,  7 April 2006 553 readers in ~2 weeks     Evolving HTTP APIs Tuesday,  4 December 2012 (/blog/http-apis/) HTTP APIs  One of the most vexing problems that still seems to be facing people when I talk to them about HTTP APIs is how to handle versioning and extensibility \u2013 i.e., how they evolve . I tend to think about this a lot and talk to quite a few people about it, since I\u2019m intimately familiar with the approaches to versioning that the HTTP protocol itself takes, and with the general attitude taken to it in the broader Internet architecture by the IETF. So, I was quite interested to come across Tom Preston-Werner\u2019s effort to define (http://semver.org/) Semantic Versioning . If you haven\u2019t seen it yet, go have a read; it\u2019s a very sensible explanation of how to evolve software. Let\u2019s apply his \u201cFiretruck\u201d example to services. If you depend on a Ladder service, you have many of the same concerns; you need an instance of it that supports the semantics you understand (major version) and the additional features that you need on top (the minor version). You might also be interested in the patch level, in case you need to do some debugging. The interesting, confusing and contentious part of applying this common sense to HTTP services is figuring out what you\u2019re versioning , and how to communicate the version . One viewpoint is to say that the service is being versioned, the service is identified by the URL, and therefore the version goes in the URL, like this: http://api.example.com/v1.2.3/ladder     However, there are (at least) two issues to consider with this approach. First of all, it\u2019s coarse-grained, in that you can\u2019t evolve parts of the system independently. For example, introducing a new format for the \u201cladder\u201d resource - by the rules, that means a minor version, which means that the URL should now become: http://api.example.com/v1.3.0/ladder     which, as discussed previously in the (/blog/2011/10/25/web_api_versioning_smackdown) API Versioning Smackdown , creates a whole new tree of resources, and tightly couples the client and server. While that may be fine if you have a very simple API with no interdependencies \u2013 such as serving a bunch of JavaScript, (http://www.nczonline.net/blog/2011/02/22/the-importance-of-being-versioned/) as Nicholas Zakas describes \u2013 it will quickly become a huge headache for testing, support and operations if you have to support all of the combinations of possible resources and their interactions in a more complex one. Second, it\u2019s also intermingling the version into identifiers. Because URIs are used in the Web as the fundamental identifier by so many things \u2013 caches, spiders, forms, and so on \u2013 embedding information that\u2019s likely to change into them make them unstable, thereby reducing their value. For example, a cache invalidates content associated with a URL when a POST request flows through it; if the URL is different because there are different versions floating around, it doesn\u2019t work. This is actually very similar to the discussion about (https://datatracker.ietf.org/doc/html/rfc6648) X- and names for HTTP headers; putting a flag into the name to signify \u201cexperimental\u201d doesn\u2019t make much sense when the experiment ends and it gets used \u201cfor real.\u201d Suggested Practices With that in mind, what should HTTP APIs do? My current thinking (based on the thinking of a lot of other people ;) is below. Keep Compatible Changes Out of Names As per above, names should be stable over time, and should identify a known set of semantics \u2013 corresponding to the major version number in Semantic Versioning. By \u201cnames,\u201d I mean everything that\u2019s used as an identifier, whether it\u2019s a URL, a media type, a link relation name, HTTP header, whatever. From what I see, most HTTP APIs are already moving in this direction, with structures like this: http://api.example.com/v2/ladder     Here, only the major version number is put in the URL; the minor and patch versions don\u2019t go in, because backwards-compatible changes don\u2019t need to be signified by changes in the name. Of course, it\u2019d be equally valid to do: http://api2.example.com/ladder     because the hostname is just as much an identifier as anything else. Even with this approach, it\u2019s worth noting that letting clients infer that it\u2019s a v2 API just from that path segment is a dodgy thing to do; however, the deeper reasons for this are the subject of a different blog post. Likewise, minor and patch versions shouldn\u2019t go into other names, such as media types or link relations. There is a train of thought that you don\u2019t need to have numeric major versions at all, since you can call the first one \u201cfoo\u201d and the second one \u201cbar\u201d \u2013 but that\u2019s just a matter of taste. Avoid New Major Versions This is also pretty widely agreed to. Every time you release a new major version, people have to look at it, understand it, write new software to it, debug it, and so on. This is a huge investment on both sides, since you also have to support two (or more) major versions concurrently for some sort of sunset period. So, new major versions should be few and far between. In a perfect world, there would be none, but the reality is that every once in a while, you need to clean up a messy API or otherwise make breaking changes. Just make them last as long as you can. Make Changes Backwards-Compatible The biggest way to avoid new major versions is to make as many of your changes backwards-compatible as possible. For example, if you want to add support for a new HTTP method, or add a new resource to the mix, this doesn\u2019t necessitate a new version. Likewise, adding support for a new format can be achieved through the miracle of content negotiation. Need to change the meaning of an existing query argument? Don\u2019t \u2013 instead, introduce a new one. Surprisingly, removing support for something can be considered backwards-compatible too. Think about it; if you remove support for, say, the \u201cfoo\u201d resource and return a 410 Gone, clients will break. However, it will only break those clients that use it; those that don\u2019t can still smoothly interoperate. Introducing a new major version to say \u201cI don\u2019t support the foo resource\u201d effectively breaks everybody , so it doesn\u2019t do any good. That naturally leads to\u2026 Think about Forwards-Compatibility Fundamentally, evolution is about figuring out how to limit the breakage that your changes incurs on clients. As such, you need to place some sorts of expectations and boundaries on how your clients should behave when they encounter certain circumstances. In other words, if a client is hardcoded to only work when \u201cfoo\u201d is there, \u201cfoo\u201d will always have to be there to avoid breaking it. More subtly, if clients don\u2019t expect an extra HTTP header, or an extra member on a JSON object, that can cause problems too, and restrict your options down the road. Expressing these expectations clearly is something we as an industry needs to get a lot better about. Unlike Web browsers \u2013 which are extremely forgiving of unexpected input \u2013 most API clients are incredibly brittle. For example, XML Schema got this pretty fundamentally wrong, making it very difficult to express a forwards-compatible schema (in 1.0). JSON is better, mostly because implementations are tied pretty closely to dynamic programming language data structures, rather than a schema language. That\u2019s not to say that you want everything to be extensible. Sometimes it\u2019s a good idea to explicitly NOT allow forward compatibility. For example, in the (https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-patch) JSON Patch format, we realised that any extensions were likely to be a fundamental change in the document semantics, thus requiring a new major version (in this case, identifying it with a new media type). After all, an old client that doesn\u2019t respect a new patch operation is going to come up with a different result than the new one that does understand it. So, we disallowed most extensions in that format. The trick is to think about it and document your expectations carefully \u2013 whether you\u2019re designing a format, a link relation type, or a HTTP header. Tell clients to expect and handle (as gracefully as they can) responses like 410 Gone, 405 Method Not Allowed, and 415 Unsupported Media Type. Version at Appropriate Granularities Going back to the example above: http://api.example.com/v2/ladder     a natural question to ask is \u201cwhat is that \u2018v2\u2019 versioning?\u201d To me, the most natural reading is that it\u2019s a collection of resources that represents a set of functionality , hierarchical URLs have collection semantics, by putting them under a single path segment (\u201cdirectory\u201d). This is an important distinction; v2 is identifying NOT just the ladder, but the whole interface, as a collection of resources (i.e., everything under that \u201cv2\u201d) that works together. Another conversation that I have sometimes is about how to relate format changes to API versions. In short, they should be completely separate; formats can have lives of their own, and to get the most value out of them, they should do so. It\u2019s fine to say \u201cVersion 2 of the API requires the foo resource to support version 5 of the bar format,\u201d of course. Make Minor and Patch Information Discoverable There are legitimate needs for minor and patch information; just because we say \u201cdon\u2019t put them into names\u201d doesn\u2019t mean that they shouldn\u2019t exist. However, I am fairly skeptical that they do much good \u201con the wire\u201d as they are. For example, consider our Ladder service, version 1.2.3. Maybe we added support for a new HTTP method in version 1.2, and fixed a few bugs in patch level 3. A self-describing service will make it completely evident (e.g., using the Allow response header) that the new method is supported; if it needs to be known about ahead of time (for example, to reflect it in a UI), you can advertise support for that method directly (e.g., with something like (https://datatracker.ietf.org/doc/html/draft-nottingham-json-home) this embryonic mechanism ). Tying this information up in a version number only makes the client go and look up a chart of version numbers to see whether the feature they want is supported by the given version; instead, if they can directly interrogate the interface to see if it supports the fancy new \u201cladder cover\u201d feature (or whatever), it\u2019s a lot more flexible and useful. The same goes for new resources, new formats, and so on. Aside from that, using linear, numeric minor versions for negotiating new features is really, really limiting; (/blog/2012/06/25/http_api_complexity_model) complex APIs will find this especially impractical. So, where should the minor and patch version numbers go? Easy \u2013 it\u2019s useful for the release notes, and a few other forms of documentation. It\u2019s useful for marketing. In the case of a more complex API (as with most standards \u2013 whether they come out of a standards body or an open source project), it\u2019s useful for packaging up an agreed-to set of functionality and calling that a spec release. Mind you, there\u2019s a strong case for including this information about the implementation of the API \u2013 server or client side \u2013 but that goes in the Server or User-Agent header respectively, and is completely separate from API versioning (i.e., you might have version 0.2.1 of the client accessing version 3.2.3 of the server\u2019s implementation of the API, which itself might have a version of 1.0.3).    (/personal/) about (/blog/) blog (/) home    "
                ],
                "output": "htmltotext.txt",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T05:00:20.834792+00:00",
                "status": "succeeded"
            }
        ],
        "media": [
            {
                "cmd": [
                    "/usr/local/bin/yt-dlp",
                    "--restrict-filenames",
                    "--trim-filenames",
                    "128",
                    "--write-description",
                    "--write-info-json",
                    "--write-annotations",
                    "--write-thumbnail",
                    "--no-call-home",
                    "--write-sub",
                    "--write-auto-subs",
                    "--convert-subs=srt",
                    "--yes-playlist",
                    "--continue",
                    "--no-abort-on-error",
                    "--ignore-errors",
                    "--geo-bypass",
                    "--add-metadata",
                    "--format=(bv*+ba/b)[filesize<=750m][filesize_approx<=?750m]/(bv*+ba/b)",
                    "--skip-download",
                    "--cache-dir=/data/yt-dlp-cache/",
                    "--cookies=/data/yt-dlp-cache/cookies.txt",
                    "--proxy=socks5://tor-socks-proxy:9150",
                    "--no-playlist",
                    "https://mnot.net/blog/2012/api-evolution"
                ],
                "cmd_version": "2024.10.7",
                "end_ts": "2026-05-30T05:00:30.231967+00:00",
                "index_texts": [],
                "output": "media/",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T05:00:23.775213+00:00",
                "status": "succeeded"
            }
        ],
        "mercury": [
            {
                "cmd": [
                    "/home/archivebox/.npm/bin/postlight-parser",
                    "https://mnot.net/blog/2012/api-evolution"
                ],
                "cmd_version": "2.2.3",
                "end_ts": "2026-05-30T05:00:20.718446+00:00",
                "index_texts": null,
                "output": "mercury/",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T05:00:08.744016+00:00",
                "status": "succeeded"
            }
        ],
        "pdf": [],
        "readability": [
            {
                "cmd": [
                    "/home/archivebox/.npm/bin/readability-extractor",
                    "/tmp/tmprkrjg4e3",
                    "https://mnot.net/blog/2012/api-evolution"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-05-30T04:59:52.010930+00:00",
                "index_texts": [
                    "Evolving HTTP APIs\n      \n\n      Tuesday,  4 December 2012\n\n      \n\n      \n      \n        \n        HTTP APIs\n        \n      \n      \n\n      \n\n      One of the most vexing problems that still seems to be facing people when I talk to them about HTTP APIs is how to handle versioning and extensibility \u2013 i.e., how they evolve.\n\nI tend to think about this a lot and talk to quite a few people about it, since I\u2019m intimately familiar with the approaches to versioning that the HTTP protocol itself takes, and with the general attitude taken to it in the broader Internet architecture by the IETF.\n\nSo, I was quite interested to come across Tom Preston-Werner\u2019s effort to define Semantic Versioning. If you haven\u2019t seen it yet, go have a read; it\u2019s a very sensible explanation of how to evolve software.\n\nLet\u2019s apply his \u201cFiretruck\u201d example to services. If you depend on a Ladder service, you have many of the same concerns; you need an instance of it that supports the semantics you understand (major version) and the additional features that you need on top (the minor version). You might also be interested in the patch level, in case you need to do some debugging.\n\nThe interesting, confusing and contentious part of applying this common sense to HTTP services is figuring out what you\u2019re versioning, and how to communicate the version. One viewpoint is to say that the service is being versioned, the service is identified by the URL, and therefore the version goes in the URL, like this:\n\nhttp://api.example.com/v1.2.3/ladder\n\n\nHowever, there are (at least) two issues to consider with this approach.\n\nFirst of all, it\u2019s coarse-grained, in that you can\u2019t evolve parts of the system independently. For example, introducing a new format for the \u201cladder\u201d resource - by the rules, that means a minor version, which means that the URL should now become:\n\nhttp://api.example.com/v1.3.0/ladder\n\n\nwhich, as discussed previously in the API Versioning Smackdown, creates a whole new tree of resources, and tightly couples the client and server.\n\nWhile that may be fine if you have a very simple API with no interdependencies \u2013 such as serving a bunch of JavaScript, as Nicholas Zakas describes \u2013 it will quickly become a huge headache for testing, support and operations if you have to support all of the combinations of possible resources and their interactions in a more complex one.\n\nSecond, it\u2019s also intermingling the version into identifiers. Because URIs are used in the Web as the fundamental identifier by so many things \u2013 caches, spiders, forms, and so on \u2013 embedding information that\u2019s likely to change into them make them unstable, thereby reducing their value. For example, a cache invalidates content associated with a URL when a POST request flows through it; if the URL is different because there are different versions floating around, it doesn\u2019t work.\n\nThis is actually very similar to the discussion about X- and names for HTTP headers; putting a flag into the name to signify \u201cexperimental\u201d doesn\u2019t make much sense when the experiment ends and it gets used \u201cfor real.\u201d\n\nSuggested Practices\n\nWith that in mind, what should HTTP APIs do? My current thinking (based on the thinking of a lot of other people ;) is below.\n\nKeep Compatible Changes Out of Names\n\nAs per above, names should be stable over time, and should identify a known set of semantics \u2013 corresponding to the major version number in Semantic Versioning. By \u201cnames,\u201d I mean everything that\u2019s used as an identifier, whether it\u2019s a URL, a media type, a link relation name, HTTP header, whatever.\n\nFrom what I see, most HTTP APIs are already moving in this direction, with structures like this:\n\nhttp://api.example.com/v2/ladder\n\n\nHere, only the major version number is put in the URL; the minor and patch versions don\u2019t go in, because backwards-compatible changes don\u2019t need to be signified by changes in the name. Of course, it\u2019d be equally valid to do:\n\nhttp://api2.example.com/ladder\n\n\nbecause the hostname is just as much an identifier as anything else.\n\nEven with this approach, it\u2019s worth noting that letting clients infer that it\u2019s a v2 API just from that path segment is a dodgy thing to do; however, the deeper reasons for this are the subject of a different blog post.\n\nLikewise, minor and patch versions shouldn\u2019t go into other names, such as media types or link relations. There is a train of thought that you don\u2019t need to have numeric major versions at all, since you can call the first one \u201cfoo\u201d and the second one \u201cbar\u201d \u2013 but that\u2019s just a matter of taste.\n\nAvoid New Major Versions\n\nThis is also pretty widely agreed to. Every time you release a new major version, people have to look at it, understand it, write new software to it, debug it, and so on. This is a huge investment on both sides, since you also have to support two (or more) major versions concurrently for some sort of sunset period. So, new major versions should be few and far between. In a perfect world, there would be none, but the reality is that every once in a while, you need to clean up a messy API or otherwise make breaking changes. Just make them last as long as you can.\n\nMake Changes Backwards-Compatible\n\nThe biggest way to avoid new major versions is to make as many of your changes backwards-compatible as possible. For example, if you want to add support for a new HTTP method, or add a new resource to the mix, this doesn\u2019t necessitate a new version. Likewise, adding support for a new format can be achieved through the miracle of content negotiation. Need to change the meaning of an existing query argument? Don\u2019t \u2013 instead, introduce a new one.\n\nSurprisingly, removing support for something can be considered backwards-compatible too. Think about it; if you remove support for, say, the \u201cfoo\u201d resource and return a 410 Gone, clients will break. However, it will only break those clients that use it; those that don\u2019t can still smoothly interoperate. Introducing a new major version to say \u201cI don\u2019t support the foo resource\u201d effectively breaks everybody, so it doesn\u2019t do any good.\n\nThat naturally leads to\u2026\n\nThink about Forwards-Compatibility\n\nFundamentally, evolution is about figuring out how to limit the breakage that your changes incurs on clients. As such, you need to place some sorts of expectations and boundaries on how your clients should behave when they encounter certain circumstances. In other words, if a client is hardcoded to only work when \u201cfoo\u201d is there, \u201cfoo\u201d will always have to be there to avoid breaking it. More subtly, if clients don\u2019t expect an extra HTTP header, or an extra member on a JSON object, that can cause problems too, and restrict your options down the road.\n\nExpressing these expectations clearly is something we as an industry needs to get a lot better about. Unlike Web browsers \u2013 which are extremely forgiving of unexpected input \u2013 most API clients are incredibly brittle. For example, XML Schema got this pretty fundamentally wrong, making it very difficult to express a forwards-compatible schema (in 1.0). JSON is better, mostly because implementations are tied pretty closely to dynamic programming language data structures, rather than a schema language.\n\nThat\u2019s not to say that you want everything to be extensible. Sometimes it\u2019s a good idea to explicitly NOT allow forward compatibility. For example, in the JSON Patch format, we realised that any extensions were likely to be a fundamental change in the document semantics, thus requiring a new major version (in this case, identifying it with a new media type). After all, an old client that doesn\u2019t respect a new patch operation is going to come up with a different result than the new one that does understand it. So, we disallowed most extensions in that format.\n\nThe trick is to think about it and document your expectations carefully \u2013 whether you\u2019re designing a format, a link relation type, or a HTTP header. Tell clients to expect and handle (as gracefully as they can) responses like 410 Gone, 405 Method Not Allowed, and 415 Unsupported Media Type.\n\nVersion at Appropriate Granularities\n\nGoing back to the example above:\n\nhttp://api.example.com/v2/ladder\n\n\na natural question to ask is \u201cwhat is that \u2018v2\u2019 versioning?\u201d\n\nTo me, the most natural reading is that it\u2019s a collection of resources that represents a set of functionality, hierarchical URLs have collection semantics, by putting them under a single path segment (\u201cdirectory\u201d).\n\nThis is an important distinction; v2 is identifying NOT just the ladder, but the whole interface, as a collection of resources (i.e., everything under that \u201cv2\u201d) that works together.\n\nAnother conversation that I have sometimes is about how to relate format changes to API versions. In short, they should be completely separate; formats can have lives of their own, and to get the most value out of them, they should do so. It\u2019s fine to say \u201cVersion 2 of the API requires the foo resource to support version 5 of the bar format,\u201d of course.\n\nMake Minor and Patch Information Discoverable\n\nThere are legitimate needs for minor and patch information; just because we say \u201cdon\u2019t put them into names\u201d doesn\u2019t mean that they shouldn\u2019t exist. However, I am fairly skeptical that they do much good \u201con the wire\u201d as they are.\n\nFor example, consider our Ladder service, version 1.2.3. Maybe we added support for a new HTTP method in version 1.2, and fixed a few bugs in patch level 3.\n\nA self-describing service will make it completely evident (e.g., using the Allow response header) that the new method is supported; if it needs to be known about ahead of time (for example, to reflect it in a UI), you can advertise support for that method directly (e.g., with something like this embryonic mechanism).\n\nTying this information up in a version number only makes the client go and look up a chart of version numbers to see whether the feature they want is supported by the given version; instead, if they can directly interrogate the interface to see if it supports the fancy new \u201cladder cover\u201d feature (or whatever), it\u2019s a lot more flexible and useful. The same goes for new resources, new formats, and so on.\n\nAside from that, using linear, numeric minor versions for negotiating new features is really, really limiting; complex APIs will find this especially impractical.\n\nSo, where should the minor and patch version numbers go? Easy \u2013 it\u2019s useful for the release notes, and a few other forms of documentation. It\u2019s useful for marketing. In the case of a more complex API (as with most standards \u2013 whether they come out of a standards body or an open source project), it\u2019s useful for packaging up an agreed-to set of functionality and calling that a spec release.\n\nMind you, there\u2019s a strong case for including this information about the implementation of the API \u2013 server or client side \u2013 but that goes in the Server or User-Agent header respectively, and is completely separate from API versioning (i.e., you might have version 0.2.1 of the client accessing version 3.2.3 of the server\u2019s implementation of the API, which itself might have a version of 1.0.3)."
                ],
                "output": "readability/",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T04:59:49.387849+00:00",
                "status": "succeeded"
            }
        ],
        "screenshot": [],
        "singlefile": [],
        "title": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://mnot.net/blog/2012/api-evolution"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-05-30T04:59:48.733368+00:00",
                "index_texts": null,
                "output": "Evolving HTTP APIs",
                "pwd": "/data/archive/1780117164.121718",
                "schema": "ArchiveResult",
                "start_ts": "2026-05-30T04:59:48.726943+00:00",
                "status": "succeeded"
            }
        ],
        "wget": []
    },
    "icons": null,
    "is_archived": true,
    "is_static": false,
    "latest": {
        "archive_org": "api-evolution.html.gzip",
        "dom": "output.html",
        "favicon": "favicon.ico",
        "git": null,
        "media": "media/",
        "pdf": null,
        "screenshot": null,
        "singlefile": null,
        "title": "Evolving HTTP APIs",
        "warc": null,
        "wget": null
    },
    "link_dir": "/data/archive/1780117164.121718",
    "newest_archive_date": "2026-05-30T05:00:30.286146+00:00",
    "num_failures": 0,
    "num_outputs": 9,
    "oldest_archive_date": "2026-05-30T04:59:29.167117+00:00",
    "path": "/blog/2012/api-evolution",
    "schema": "Link",
    "scheme": "https",
    "snapshot_abid": "snp_01KSVM0M4M85A2E6F601EM1C5M",
    "snapshot_id": "b0a33825-eb97-4027-93d7-2f5e5d40b0b4",
    "sources": [
        "/data/sources/1780117162-import.txt"
    ],
    "tags": null,
    "tags_str": "",
    "timestamp": "1780117164.121718",
    "title": "Evolving HTTP APIs",
    "url": "https://mnot.net/blog/2012/api-evolution"
}