When a client requests data from an API it may only want a subset of available fields, but many APIs only offer a single representation of available data. We can use Content Negotiation for more than just choosing between json or xml (or whatever) and provide exactly what a client asks for.
A Sample API
Many applications use the concept of a collection of items that belong to a user, so here we'll use a collection of favourite colours1.
We can define a simple
my-colours endpoint in F# using
This defines a
Colour type that contains a few different
properties, and makes a collection of them available via
request. It returns a hardcoded list of values, but it's just an
example so that's ok.
We can also create a simple client to retrieve the data from the endpoint
and display it to an end user. The sample swift code is part of an iOS app
URLSession and the
Decodable protocol to
make things easy:
This works pretty well, but we can already see that there's a mismatch between the data that the API provides and the data that the client actually needs - the API returns a bunch of fields that the client doesn't care about. For this sample API it's not much of an issue, but if we had a lot of entries in the list or our colour object was more complex it could become a problem. Fortunately, we have some ways to match our API to the needs of the client.
A New Media Type
We can't just remove values from our API response because there may be other clients relying on that data, but if we provide a way for a client to ask for a different response then we can give each client a response containing the data that it needs.
First, we need to configure Giraffe to support custom media types. We don't need any custom serialization for this, so we can just use the json serializer that's already configured.
Then we can create a custom representation of
and return it from our endpoint for clients that request the mini
With our API configured to return the mini colour response to
clients that request it, we can update our app client to send the
Accept header. Handily, for our swift client it's
trivial to set an extra header:
Now our client requests a specific response and our API provides it, which is great...for now. However, this does leave our API and client coupled together - if our clients needs change in the future, then our API will also need to be updated even if it could already provide the field, our only options are all or nothing.
Asking For What We Need
We want to find a way that our client can adjust the data it gets from the API as it needs without us needing to make changes to the API. This means that we'll need some way for a client to describe the data that it wishes to receive from the API.
There are a few existing ways of describing the shape of data an API should return to a client, notably GraphQL and JSON:API, so it might be worth considering how to implement this in a way that doesn't necessarily tie us to any particular implementation in the future in case we change preference later on, or especially in the case that we have different API clients with differing needs.
Introspected ReST suggests using MicroTypes as a way of composing a media type. Using that approach, we can add one method of filtering now and then add support for different approaches later without breaking any clients. So we'll need to make the following changes:
- Switch our API to use a media type that can support filtering
We'll use a general api media type:
application/vnd.chamook.api+jsonthat could work across any apis connected with this one. We don't use something like
application/jsonbecause we can't guarantee that an API returning plain old json will support filtering (or any other features we wanted to add).
- Define a filtering syntax that our client can use
- We could use one of the fancy options mentioned earlier. But for now we only want a very simple filter, so a custom field list will probably be easier to implement. Because we're using MicroTypes we could always add support for something fancy like GraphQL later if it turned out we needed it.
- Update our client to use the new filtering method
- Once this update is done, our client and API should be nicely decoupled for any future changes to filtering.
We could potentially add on ways for the API to describe available filtering options to a client, but if we don't want to go all in on a ReSTful approach with this API we could just leave that as something to be included in documentation - as long as we follow common conventions across our API surface it shouldn't be too problematic.
The Final Form(s)
With that decided, we can update our API with the following code (using the simplest filtering approach, by specifying which fields should be included in the response as a parameter to the media type):
Our client needs only minor changes to use the new version:
Now our client can specify exactly the fields it wants, and our API will respond appropriately. There are still things that could be improved, but this is enough for now.
The source code for the API and the app client are available on GitHub.