mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-27 13:01:03 +00:00
new essay
This commit is contained in:
parent
5083393a9b
commit
29bb2abcc6
@ -31,6 +31,7 @@ page_template = "essay.html"
|
||||
* [Hypermedia-Driven Applications (HDAs)](@/essays/hypermedia-driven-applications.md)
|
||||
* [Hypermedia Friendly Scripting](@/essays/hypermedia-friendly-scripting.md)
|
||||
* [10 Tips For Building SSR/HDA applications](@/essays/10-tips-for-SSR-HDA-apps.md)
|
||||
* [Why I Tend Not To Use Content Negotiation](@/essays/why-tend-not-to-use-content-negotiation.md)
|
||||
* [Template Fragments](@/essays/template-fragments.md)
|
||||
* [View Transitions](@/essays/view-transitions.md)
|
||||
|
||||
|
@ -24,7 +24,7 @@ Hypermedia APIs:
|
||||
Data APIs, on the other hand:
|
||||
|
||||
* Will not benefit dramatically from REST-fulness, beyond perhaps [Level 2 of the Richardson Maturity Model](https://en.wikipedia.org/wiki/Richardson_Maturity_Model)
|
||||
* Should strive for both regularity and expressivity due to the arbitrary data needs of consumers
|
||||
* Should strive for both regularity and expressiveness due to the arbitrary data needs of consumers
|
||||
* Should be versioned and should be very stable within a particular version of the API
|
||||
* Should be consumed by code, processed and then potentially presented to a human
|
||||
|
||||
|
177
www/content/essays/why-tend-not-to-use-content-negotiation.md
Normal file
177
www/content/essays/why-tend-not-to-use-content-negotiation.md
Normal file
@ -0,0 +1,177 @@
|
||||
+++
|
||||
title = "Why I Tend Not To Use Content Negotiation"
|
||||
date = 2023-11-18
|
||||
updated = 2023-11-18
|
||||
[taxonomies]
|
||||
author = ["Carson Gross"]
|
||||
tag = ["posts"]
|
||||
+++
|
||||
|
||||
I have written a lot about Hypermedia APIs vs. Data (JSON) APIs, including [the differences between the two](@/essays/hypermedia-apis-vs-data-apis.md),
|
||||
what [REST "really" means](@/essays/how-did-rest-come-to-mean-the-opposite-of-rest.md) and why [HATEOAS](@/essays/hateoas.md)
|
||||
isn't so bad as long as your API is interacting with a [Hypermedia Client](@/essays/hypermedia-clients.md).
|
||||
|
||||
Often when I am engaged in discussions with people coming from the "REST is JSON over HTTP" world (that is, the normal
|
||||
world) I have to navigate a lot of language and conceptual issues:
|
||||
|
||||
* No, I am not advocating you return HTML as a general purpose API, hypermedia makes for a bad general purpose API
|
||||
* Yes, I am advocating [tightly coupling](@/essays/two-approaches-to-decoupling.md) your web application to your hypermedia API
|
||||
* No, I do not think that we will ever fix how the industry [uses the term REST](@/essays/how-did-rest-come-to-mean-the-opposite-of-rest.md)
|
||||
* Yes, I am advocating you [split your data API and your hypermedia API up](@/essays/splitting-your-apis.md)
|
||||
|
||||
The last point often strikes people who are used to a single, general purpose JSON API as dumb: why have two APIs when you
|
||||
can have a single API that can satisfy any number of types of clients? I tried to answer that question as best I can in the essay
|
||||
above, but it is certainly a reasonable one to ask.
|
||||
|
||||
It seems like (and it is) extra work in some ways when compared to have that one, general API and the associated logic.
|
||||
|
||||
At this point in a conversation, someone who agrees broadly with my take on REST, [Hypermedia-Driven Applications](@/essays/hypermedia-driven-applications.md),
|
||||
etc. will often jump in and say something like
|
||||
|
||||
> "Oh, it's easy, you just use _content negotiation_, it's baked into HTTP!"
|
||||
|
||||
Not being content with alienating only the general purpose JSON API enthusiasts, let me know proceed to also alienate
|
||||
my erstwhile hypermedia enthusiast allies by saying:
|
||||
|
||||
*I don't think content negotiation is typically the right approach to
|
||||
returning both JSON and HTML for most applications.*
|
||||
|
||||
## What Is Content Negotiation?
|
||||
|
||||
First things first, what is "content negotiation"?
|
||||
|
||||
[Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) is a feature of HTTP that
|
||||
allows a client to negotiate the content type of the response from a server. A full treatment of the implementation
|
||||
in HTTP is beyond the scope of this essay, but let us consider the most well known mechanism for content negotiation
|
||||
in HTTP, the [`Accept` Request Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#the_accept_header).
|
||||
|
||||
The `Accept` request header allows a client, such as a browser, to indicate the `MIME` types that it is willing to accept
|
||||
from the server in a response.
|
||||
|
||||
An example value of this header is:
|
||||
|
||||
```http request
|
||||
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8
|
||||
```
|
||||
|
||||
This `Accept` header tells the server what formats the client is willing to accept. Preferences are expressed via the
|
||||
`q` weighting factor. Wildcards are expressed with asterisks `*`.
|
||||
|
||||
In this case, the client is saying:
|
||||
|
||||
> I would most like to receive text/html, application/xhtml+xml or image/webp. Next I would prefer application/xml. Finally, I will accept whatever you give me.
|
||||
|
||||
The server then can take this information and determine the best content type to provide to the client.
|
||||
|
||||
This is the act of "content negotiation" and it is certainly an interesting feature of HTTP.
|
||||
|
||||
## Using Content Negotiation In APIs
|
||||
|
||||
As far as I am aware, it was the [Ruby On Rails](https://rubyonrails.org/) community that first went in in a big way
|
||||
using content negotiation to provide both HTML and JSON (and other) formats from the same URL.
|
||||
|
||||
In Rails, this is accomplished via the [`respond_to`](https://apidock.com/rails/ActionController/MimeResponds/respond_to) helper method available in
|
||||
controllers.
|
||||
|
||||
Leaving the gory details of Rails aside, you might have a request like an HTTP `GET` to `/contacts` that ends up invoking
|
||||
a function in a `ContactsController` class that looks like this:
|
||||
|
||||
```ruby
|
||||
def index
|
||||
@contacts = Contacts.all
|
||||
|
||||
respond_to do |format|
|
||||
format.html # default rendering logic
|
||||
format.json { render json: @contacts }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
By making use of the `respond_to` helper method, if a client makes a request with the `Accept` header above, the controller
|
||||
will render an HTML response using the Rails templating systems.
|
||||
|
||||
However, if the `Accept` header from the client has the value `application/json` instead, Rails will render the contacts
|
||||
as a JSON array for the client.
|
||||
|
||||
A pretty neat trick: you can keep all your controller logic, like looking up the contacts, the same and just use a
|
||||
bit of ruby/Rails magic to render two different response types using content negotiation. Barely any additional work on
|
||||
top of the normal Model/View/Controller logic.
|
||||
|
||||
You can see why people like the idea!
|
||||
|
||||
## So What's The Problem?
|
||||
|
||||
So why don't I think this is a good approach to splitting your JSON and HTML APIs up?
|
||||
|
||||
It boils down to the [differences between JSON APIs and Hypermedia (HTML) APIs](hypermedia-apis-vs-data-apis.md) I hinted
|
||||
at earlier. In particular:
|
||||
|
||||
* Data APIs should be versioned and should be very stable within a particular version of the API
|
||||
* Data APIs should strive for both regularity and expressiveness due to the arbitrary data needs of consumers
|
||||
* Data APIs typically use some sort of token-based authentication
|
||||
* Data APIs should be rate limited
|
||||
* Hypermedia APIs typically use some sort of session-cookie based authentication
|
||||
* Hypermedia APIs should be driven by the needs of the underlying hypermedia application
|
||||
|
||||
While all of these differences matter and have an effect on your controller code, pulling it in two different directions,
|
||||
it is really the first and last items that make me often choose not to use content negotiation in my applications.
|
||||
|
||||
Your JSON API needs to be a stable set of endpoint that client code can rely on.
|
||||
|
||||
Your hypermedia API, on the other hand, can change dramatically based on the user interface needs of your applications.
|
||||
|
||||
These two things don't mix well.
|
||||
|
||||
To give you a concrete example, consider an end point that renders a detail view of a contact, at, say `/contacts/:id`
|
||||
(where `:id` is a parameter containing the id of the contact to render). Let's say that this page has a "related contacts"
|
||||
section of the UI and, further, computing these related contacts is expensive for some reason.
|
||||
|
||||
In this situation you might choose to use the [Lazy Loading](https://htmx.org/examples/lazy-load/) pattern to defer
|
||||
loading the related contacts until after the initial contact detail screen has been rendered. This improves perceived
|
||||
performance of the page for your users.
|
||||
|
||||
If you did this, you might put the lazy loaded content at the end-point `/contacts/:id/related`.
|
||||
|
||||
Now, later on, maybe you are able to optimize the computation of related contacts. At this point you might choose to
|
||||
rip the `/contacts/:id/related` end-point out and just render the related contacts information in the initial page render.
|
||||
|
||||
All of this is fine for your hypermedia API: hypermedia, through [the uniform interface & HATEOAS](@/essays/hateoas.md)
|
||||
is _designed_ to handle these sorts of changes.
|
||||
|
||||
However, your JSON API... not so much.
|
||||
|
||||
Your JSON API should remain stable. You can't be adding and removing end-points
|
||||
willy-nilly. Yes, you can have _some_ end-points respond with either JSON or HTML and others only respond with HTML, but
|
||||
it gets messy. What if you accidentally copy-and-paste in the wrong code somewhere, for example.
|
||||
|
||||
Taking all of this into account, as well as things like rate-limiting and so on, I think you can make a strong argument
|
||||
that there should be a [Separation Of Concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) between the JSON
|
||||
API and the hypermedia API.
|
||||
|
||||
(Yes, I am aware of the irony that the person who coined the term [Locality of Behaviour](@/essays/locality-of-behaviour.md)
|
||||
is making a SoC argument.)
|
||||
|
||||
## So What's The Alternative?
|
||||
|
||||
The alternative is to, as I advocate in [Splitting Your APIs](@/essays/splitting-your-apis.md), erm, splitting your
|
||||
APIs. This means providing different paths (or sub-domains, or whatever) for your JSON API and your hypermedia (HTML)
|
||||
API.
|
||||
|
||||
Going back to our contacts API, we might have the following:
|
||||
|
||||
* The JSON API to get all contacts is found at `/api/v1/contacts`
|
||||
* The Hypermedia API to get all contacts is found at `/contacts`
|
||||
|
||||
This layout implies two different controllers and, I say, that's a good thing: the JSON API controller can implement the
|
||||
requirements of a JSON API: rate limiting, stability, maybe an expressive query mechanism like GraphQL.
|
||||
|
||||
Meanwhile, your
|
||||
hypermedia API (really, just your Hypermedia Driven Application endpoints) can change dramatically as your user interface
|
||||
needs change, with highly tuned database queries, end-points to support special UI needs, etc.
|
||||
|
||||
By separating these two concerns, your JSON API can be stable, regular and low-maintenance, and your hypermedia API can
|
||||
be chaotic, specialized and flexible. Each gets its own controller environment to thrive in, without conflicting with
|
||||
one another.
|
||||
|
||||
And this is why I prefer to split my JSON and hypermedia APIs up into separate controllers, rather than use HTTP content
|
||||
negotiation to attempt to reuse controllers for both.
|
Loading…
x
Reference in New Issue
Block a user