Apparatus

A library for managing hyperlinks in modern Python web applications

This article introduces hrefs, a handy Python library I originally authored in 2021. hrefs makes it easy to manage hyperlinks in REST APIs in applications using pydantic models. The prime reason for its existence is developing FastAPI apps, but as I hope to demonstrate, it’s possible to integrate it with multiple web frameworks.

Modeling relationships in a REST API

The easiest way for a developer to model relationships between resources is by including foreign keys in the schemas returned by the API. Even the official FastAPI documentation page discussing relational databases does it by making the Item model contain an owner_id field that refers to its owner.

from pydantic import BaseModel

class Item(BaseModel):
    id: int
    owner_id: int

This results in the following schema:

GET /items/123 HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "owner_id": 1
}

But that feels very database-y. As argued in this article, the best way to model relationships in a REST API over HTTP is by using hyperlinks. When using hyperlinks, the schema would be like the following:

GET /items/123 HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "owner": "http://my-awesome.app/people/1"
}

The hyperlink style is better for several reasons. It brings more abstraction and decouples your API schemas from the underlying relational databases. Hyperlinks are self-documenting. A client can readily follow the hyperlinks without knowing your URL structure. And if you ever need to change your URL structure, clients relying on hyperlinks returned by the server are much easier to update.

The drawback of using hyperlinks is the extra effort it needs from a developer. Instead of just reading fields from a database and dumping them into the model constructor, they need to write more code to convert between the database and API models. And vice versa: When parsing data containing URLs, they need to convert back to keys to use them in the database context again. The conventional wisdom in software engineering is the less code you write, the fewer bugs.

Declarative hyperlinks

When I developed REST APIs in Django, I acquainted myself with the Django REST framework that had solved the relationships elegantly with a declarative way to define how to serialize relationships. A developer can use HyperlinkedRelatedField in their serializers, give a few configurations, and have their models and database keys magically convert into correct hyperlinks.

Django and the Django REST framework are massive and opinionated, have years of legacy, and don’t properly work with the asynchronous programming paradigms. I wanted something as easy, or even easier, to use than HyperlinkedRelatedField, but lighter and more modular.

And that something was, you guessed it, the hrefs library.

Crash course to hrefs

With hrefs, you define your model like this:

from hrefs import Href, BaseReferrableModel

class Item(BaseReferrableModel):
    id: int
    owner: Href[Person]

The library takes it from there and ensures that URLs get deserialized on read and serialized on write. A Href object isn’t just an URL. It’s a bridge between a model key and a model URL. It can not only parse a model key (an integer in the above example) and spit out an URL. It can also do the converse: parse an URL and spit out the key. Hence it can be used both for deserializing URLs from requests and serializing them into responses.

>>> item1 = Item(id=123, owner=1)
>>> item1.owner.url
"https://example.com/people/1"
>>> item2 = Item(id=123, owner="https://example.com/people/1")
>>> item2.owner.key
1

The documentation has an example of a toy app with the Href type taken into use in a FastAPI app.

Naturally, the model key can have other types than just integers. It can be anything serializable in the path or query parameters of an URL: a UUID, a slug, or whatever suits your need. A key can even consist of multiple parts, and the library automatically serializes and deserializes tuples representing the parts.

I’ve tried to design hrefs to be as modular as possible. The core BaseReferrableModel and its metaclass are responsible for figuring out the model key and implementing the generic algorithms to work between the key and URL representation. Of course, it needs help from the web framework and integrates them with a well-defined protocol. Currently, only Starlette and (by extension) FastAPI are supported, but authoring integration to another web framework is possible.

The library couples tightly to pydantic due to its powerful parsing engine. Unfortunately, that means rewriting it for attrs or another dataclass library would take a more substantial effort.

Next steps

I haven’t kept any noise about the library, but I have noticed some traffic on the GitHub and PyPi pages. I hope and assume it means someone besides me is using the library even now. If you’re one of them, I’d love to hear from you! How does it work for you, what could be improved, and what features are still missing?

I occasionally have ideas I collect into the issue tracker. These are mainly driven by how to make writing my applications easier. I don’t see myself switching to other web frameworks, and there are probably use cases and features that the hrefs library could support better. If you need anything, please shoot a message in the issue tracker or, even better, a PR!