HTTP/1.1: The Protocol That Ran for 18 Years
HTTP/1.1 (RFC 2068, 1997) added persistent connections, chunked transfer encoding, the Host header, and full cache control directives. The most important contribution was semantic: PUT, PATCH, DELETE, and OPTIONS gave HTTP a complete CRUD vocabulary. Virtually the entire internet ran on HTTP/1.1 from 1997 to 2015 without a major revision.
HTTP/1.1 formalized the method semantics that API design interviews test. GET is safe and idempotent - calling it ten times should have the same effect as calling it once, and it should not modify state. PUT is idempotent but not safe - calling it twice should produce the same result. POST is neither safe nor idempotent. DELETE is idempotent. PATCH is neither.
Content Negotiation: One Endpoint, Multiple Formats
HTTP/1.1 also formalized content negotiation. Clients could send "Accept: application/json" and the server would return JSON. Send "Accept: application/xml" and get XML. The same endpoint, the same resource, different representations. This is the REST principle of resource-representation separation: your /users/123 resource exists independently of whether you serialize it as JSON, XML, or MessagePack.
XML: Power Without Usability
XML arrived in 1998 as the W3C standard for a universal data format. The spec was serious: self-describing tags, XSD schemas for validation, namespaces for composing multiple schemas, XSLT for transformation, XPath for querying. Every enterprise adopted it. Every integration used it. SAP, Oracle, IBM - all XML, everywhere.
A user object that is 50 bytes in JSON is 200+ bytes in XML after opening tags, closing tags, namespaces, and schema declarations. Parsing required DOM or SAX parsers. A JavaScript developer who could read JSON intuitively needed to learn an entirely separate query language (XPath) to work with XML data. The specification was complete. The experience was painful.
XML had namespaces, schemas, transformations, and validation - every feature you could ask for. JSON had simplicity. JSON won because developers could read it, write it, and parse it in one line. When you design an API, optimize for the person reading the docs at 2am.
HTTP Caching: The Most Underused API Feature
HTTP/1.1 added a complete cache control system that most API developers ignore. The Cache-Control header lets you specify whether a response is cacheable, for how long, by whom (public caches like CDNs vs private caches like browsers), and under what conditions it must be revalidated. ETags let clients check if a cached response is still fresh without downloading the full response - the server returns 304 Not Modified and the client uses its cached copy. The Vary header tells caches that the cached response is specific to particular request headers (like Accept-Language or Accept-Encoding).
A read-heavy API that correctly implements caching can serve 95% of requests from CDN edge nodes without the request ever reaching the origin server. This is not a premature optimization - it is a correctness concern. If your API returns user-specific data with a public cache-control directive, you will leak data between users. If your API returns frequently changing data with a long max-age, you will serve stale data. Getting caching semantics right is a first-order API design concern, not an infrastructure detail.
Content Negotiation as API Versioning
HTTP/1.1's Accept header enables a versioning strategy that avoids URL pollution. Instead of versioning via /api/v1/users and /api/v2/users, you can version via the Accept header: Accept: application/vnd.myapi.v2+json. The URL stays stable; the representation changes. GitHub uses this approach for their API (Accept: application/vnd.github.v3+json). The advantage is that the resource URL is a stable identity across versions. The disadvantage is that version negotiation is invisible in browser address bars and harder to test manually.
There is no universally correct API versioning strategy. URL versioning (/v1/) is explicit and cache-friendly. Header versioning is more RESTful and cleaner in the URL namespace. Query parameter versioning (?version=1) is the most accessible but pollutes query strings. The important thing is picking one strategy and applying it consistently. APIs that mix versioning strategies across endpoints are the worst of all worlds.
- Use Cache-Control: no-store for responses containing sensitive user data that should never be cached anywhere.
- Use Cache-Control: public, max-age=3600 for public content that can safely be cached by CDNs.
- Use Cache-Control: private, max-age=300 for user-specific responses that should only be cached by the requesting browser.
- Implement ETags for expensive-to-compute but infrequently-changing resources. The ETag is typically a hash of the response body.
- The Vary header is critical for APIs that return different responses based on Accept headers. Without Vary: Accept, a CDN might cache the JSON response and serve it to clients that asked for XML.
XML's Lasting Influence on Modern API Tooling
XML's influence on API tooling is more pervasive than its direct presence suggests. XSD (XML Schema Definition) was the first widely adopted schema language for API contracts. JSON Schema is its spiritual successor, using the same concepts (types, required fields, string patterns, numeric constraints) in a more accessible syntax. XSLT (XML transformation language) inspired the JSONata and jq transformation tools. XPath (XML query language) is the conceptual ancestor of JSONPath, which is used in OpenAPI for response body filtering and in tools like Kubernetes' kustomize for patch targets.
XML's namespace system - a mechanism for combining schema elements from multiple sources without naming collisions - failed in practice but motivated JSON-LD (JSON for Linked Data), which solves the same vocabulary collision problem for JSON using @context declarations. JSON-LD is the data format used in schema.org markup, Google's structured data specifications, and the Activity Streams protocol used by federated social networks like Mastodon. The problems XML tried to solve are real problems; JSON-LD solves them with better tooling and simpler syntax.
HTTP/1.1's Persistent Connection Model and Why It Mattered
HTTP/1.0 opened a new TCP connection for every request and closed it after the response. TCP connection setup involves a three-way handshake that adds one round-trip latency before the first byte of data is sent. For a webpage with 30 resources (HTML, CSS, JS, images), HTTP/1.0 required 30 separate connection handshakes. HTTP/1.1's persistent connections (keep-alive) reused the same TCP connection for multiple requests, reducing this overhead dramatically. The Host header that HTTP/1.1 required (to support multiple virtual hosts on one IP address) is present in every HTTP request you have ever made.
HTTP/1.1's pipelining feature - sending multiple requests without waiting for responses - was theoretically a major performance improvement but practically unusable because head-of-line blocking (a slow response blocks all subsequent responses on the connection) made it unreliable. Browser vendors disabled it by default. This limitation was not fixed until HTTP/2's multiplexed streams, which give each request its own independent stream within the shared connection. Understanding this progression helps you understand why HTTP/2 adoption was a genuine performance improvement, not just a version bump.
HTTP Caching: The Most Underused API Feature
HTTP/1.1 added a complete cache control system that most API developers ignore. The Cache-Control header lets you specify whether a response is cacheable, for how long, by whom (public caches like CDNs vs private caches like browsers), and under what conditions it must be revalidated. ETags let clients check if a cached response is still fresh without downloading the full response - the server returns 304 Not Modified and the client uses its cached copy. The Vary header tells caches that the cached response is specific to particular request headers (like Accept-Language or Accept-Encoding).
A read-heavy API that correctly implements caching can serve 95% of requests from CDN edge nodes without the request ever reaching the origin server. This is not a premature optimization - it is a correctness concern. If your API returns user-specific data with a public cache-control directive, you will leak data between users. If your API returns frequently changing data with a long max-age, you will serve stale data. Getting caching semantics right is a first-order API design concern, not an infrastructure detail.
Content Negotiation as API Versioning
HTTP/1.1's Accept header enables a versioning strategy that avoids URL pollution. Instead of versioning via /api/v1/users and /api/v2/users, you can version via the Accept header: Accept: application/vnd.myapi.v2+json. The URL stays stable; the representation changes. GitHub uses this approach for their API (Accept: application/vnd.github.v3+json). The advantage is that the resource URL is a stable identity across versions. The disadvantage is that version negotiation is invisible in browser address bars and harder to test manually.
There is no universally correct API versioning strategy. URL versioning (/v1/) is explicit and cache-friendly. Header versioning is more RESTful and cleaner in the URL namespace. Query parameter versioning (?version=1) is the most accessible but pollutes query strings. The important thing is picking one strategy and applying it consistently. APIs that mix versioning strategies across endpoints are the worst of all worlds.
- Use Cache-Control: no-store for responses containing sensitive user data that should never be cached anywhere.
- Use Cache-Control: public, max-age=3600 for public content that can safely be cached by CDNs.
- Use Cache-Control: private, max-age=300 for user-specific responses that should only be cached by the requesting browser.
- Implement ETags for expensive-to-compute but infrequently-changing resources. The ETag is typically a hash of the response body.
- The Vary header is critical for APIs that return different responses based on Accept headers. Without Vary: Accept, a CDN might cache the JSON response and serve it to clients that asked for XML.
XML's Lasting Influence on Modern API Tooling
XML's influence on API tooling is more pervasive than its direct presence suggests. XSD (XML Schema Definition) was the first widely adopted schema language for API contracts. JSON Schema is its spiritual successor, using the same concepts (types, required fields, string patterns, numeric constraints) in a more accessible syntax. XSLT (XML transformation language) inspired the JSONata and jq transformation tools. XPath (XML query language) is the conceptual ancestor of JSONPath, which is used in OpenAPI for response body filtering and in tools like Kubernetes' kustomize for patch targets.
XML's namespace system - a mechanism for combining schema elements from multiple sources without naming collisions - failed in practice but motivated JSON-LD (JSON for Linked Data), which solves the same vocabulary collision problem for JSON using @context declarations. JSON-LD is the data format used in schema.org markup, Google's structured data specifications, and the Activity Streams protocol used by federated social networks like Mastodon. The problems XML tried to solve are real problems; JSON-LD solves them with better tooling and simpler syntax.
HTTP/1.1's Persistent Connection Model and Why It Mattered
HTTP/1.0 opened a new TCP connection for every request and closed it after the response. TCP connection setup involves a three-way handshake that adds one round-trip latency before the first byte of data is sent. For a webpage with 30 resources (HTML, CSS, JS, images), HTTP/1.0 required 30 separate connection handshakes. HTTP/1.1's persistent connections (keep-alive) reused the same TCP connection for multiple requests, reducing this overhead dramatically. The Host header that HTTP/1.1 required (to support multiple virtual hosts on one IP address) is present in every HTTP request you have ever made.
HTTP/1.1's pipelining feature - sending multiple requests without waiting for responses - was theoretically a major performance improvement but practically unusable because head-of-line blocking (a slow response blocks all subsequent responses on the connection) made it unreliable. Browser vendors disabled it by default. This limitation was not fixed until HTTP/2's multiplexed streams, which give each request its own independent stream within the shared connection. Understanding this progression helps you understand why HTTP/2 adoption was a genuine performance improvement, not just a version bump.

