Home > Software engineering >  Building the links key in RESTful response (for HATEOAS)
Building the links key in RESTful response (for HATEOAS)

Time:10-06

I created a geo API for Portugal and now I am trying to conform RESTful standards by introducing HATEOAS.

Example: this response is a list of parishes (freguesias) for a specific municipality (municipio) /municipios/{municipality}/freguesias

{
  "nome": "Porto",
  "freguesias": [
    "Bonfim",
    "Campanhã",
    "Paranhos",
    "Ramalde",
    "União das freguesias de Aldoar, Foz do Douro e Nevogilde",
    "União das freguesias de Cedofeita, Santo Ildefonso, Sé, Miragaia, São Nicolau e Vitória",
    "União das freguesias de Lordelo do Ouro e Massarelos"
  ]
}

Each parish (element of Array freguesias) has its own link: /freguesias/{parish}

How would you build the links key in the response?

CodePudding user response:

The simplest answer is something like:

{
  "nome": "Porto",
  "uri": "/municipios/Porto",
  "freguesias": [
    {"uri": "/municipios/Porto/freguesias/Bonfim", "name": "Bonfim"},
    {"uri": "/municipios/Porto/freguesias/Campanhã", "name" "Campanhã"},
    ...
  ]
}

Though if somebody writes this, then they are missing the point that we are following hyperlinks here and when you expand the object graph here with one more level, then you follow hyperlinks automatically.

The classical solution to overcome the resource id - link problem is adding a self reference.

{
    "nome": "Porto",
    "links": [
        {"relation": "self", "uri": "/municipios/Porto"}
    ],
    "freguesias": [
        {
            "name": "Bonfim",
            "links": [
                {"relation": "self", "uri": "/municipios/Porto/freguesias/Bonfim"}
            ]
        },
        {
            "name" "Campanhã",
            "links": [
                {"relation": "self", "uri": "/municipios/Porto/freguesias/Campanhã"}
            ]
        },
        ...
    ]
}

Self is a link relation here, it describes the relationship of the resource identified by the links URI with the resource that contains the link. You can find standard link relations here: https://www.iana.org/assignments/link-relations/link-relations.xhtml

Another approach I like a lot better is describing the operation which happens when you follow a link including non-GET links, like POST, etc... too.

{
    "nome": "Porto",
    "links": [
        {"operation": "GetMunicipality", "uri": "/municipios/Porto", "method": "GET"}
    ],
    "freguesias": [
        {
            "name": "Bonfim",
            "links": [
                {"operation": "GetMunicipalityParish", "uri": "/municipios/Porto/freguesias/Bonfim", "method": "GET"}
            ]
        },
        {
            "name" "Campanhã",
            "links": [
                {"operation": "GetMunicipalityParish", "uri": "/municipios/Porto/freguesias/Campanhã", "method": "GET"}
            ]
        },
        ...
    ]
}

It is not necessary to have a links array, especially in this second case, because the relation names are general, while the operation names are API specific, so it is unlikely they would interfere with property names.

{
    "nome": "Porto",
    "GetMunicipality": {"uri": "/municipios/Porto", "method": "GET"}
    "freguesias": [
        {
            "name": "Bonfim",
            "GetMunicipalityParish": {"uri": "/municipios/Porto/freguesias/Bonfim", "method": "GET"}
        },
        {
            "name" "Campanhã",
            "GetMunicipalityParish": {"uri": "/municipios/Porto/freguesias/Campanhã", "method": "GET"}
        },
        ...
    ]
}

It is harder to process it this way automatically, but if you like this flatter approach better, then you can add object types.

{
    "type": "Municipality",
    "nome": "Porto",
    "GetMunicipality": {"type": "Link", "uri": "/municipios/Porto", "method": "GET"}
    "freguesias": [
        {
            "type": "Parish",
            "name": "Bonfim",
            "GetMunicipalityParish": {"type": "Link", "uri": "/municipios/Porto/freguesias/Bonfim", "method": "GET"}
        },
        {
            "type": "Parish",
            "name" "Campanhã",
            "GetMunicipalityParish": {"type": "Link", "uri": "/municipios/Porto/freguesias/Campanhã", "method": "GET"}
        },
        ...
    ]
}

As of the list of parishes, it is a different resource than the municipality, so instead of using an array you need to use an object there too and add something like items or members.

{
    "type": "Municipality",
    "nome": "Porto",
    "GetMunicipality": {"type": "Link", "uri": "/municipios/Porto", "method": "GET"}
    "freguesias": {
        "ListMunicipalityParishes": {"type": "Link", "uri": "/municipios/Porto/freguesias", "method": "GET"},
        "type": "ParishList",
        "members": [
            {
                "type": "Parish",
                "name": "Bonfim",
                "GetMunicipalityParish": {"type": "Link", "uri": "/municipios/Porto/freguesias/Bonfim", "method": "GET"}
            },
            {
                "type": "Parish",
                "name" "Campanhã",
                "GetMunicipalityParish": {"type": "Link", "uri": "/municipios/Porto/freguesias/Campanhã", "method": "GET"}
            },
            ...
        ]
    }
}

The first one is close to the HAL approach, the second one is close to the Hydra approach. Now if you think this further, from a different perspective you get the same info with less requests if you expand properties instead of following hyperlinks related to them and these self-referencing links appear to be on the wrong level in this data structure, e.g. you won't call ListMunicipalityParishes when you already got the list. So it can be simplified this way:

api.GetMunicipality("Porto") -> GET /municipios/Porto

{
    "@type": "Municipality",
    "nome": "Porto",
    "ListMunicipalityParishes": {
        "@type": "Link",
        "uri": "/municipios/Porto/freguesias",
        "method": "GET",
        "response": [
                {
                    "@type": "Parish",
                    "name": "Bonfim"
                },
                {
                    "@type": "Parish",
                    "name" "Campanhã"
                },
                ...
        ]
    }
}

You can use the current frameworks or try your custom one.

CodePudding user response:

In a narrow sense REST is not based on standards other than HTTP as its main transport layer, URI as its naming scheme and various well-defined media-type definitions which it uses to exchange messages with peers that are able to process such. REST is an architectural style, a set of indirections if you will that decouple clients from servers allowing a server to effectively evolve freely without breaking clients as these are inherently designed with change in mind.

HATEOAS is nothing more than an abbreviation for hypertext as the engine of application state meaning that the media-type exchanged should allow clients to progress their task without having to contact or lookup external documentation. This is either achieved via attaching URIs to link-relation names, which allows the URI to be swapped over time and clients still being able to lookup the URI to "invoke" via the relation-name, or by returning a representation format that contains elements that "teach" a client on how to construct requests.

In regards to link-relation names, these should be either based on registered names such as first, last, next, prev or up, or define custom ones following the Web Linking extension mechanism. In the latter case you use a URI that does not necessarily need to point to a resource or documentation, like i.e. https://acme.com/rel/parent. This basically acts as the predicate in a Semantic Web triple which sets the target resource identified by the attached URI in context to the current resource. A URI may even be attached to multiple link-relation names. In case a client does not understand a certain link-relation name it should ignore that relation name. Such relation names may be further defined in media-types or profiles.

A server can "teach" clients on how to construct requests through form representations or elements defined by a media-type. I.e. think of HTLM form. The HTML form does explain a client how a request sent to the server should look like. It describes the properties a resource supports and the server expects as input along the HTTP method to use upon sending the request, as well as the target URI to send the request to and the representation format to marshal the request to. This is usually implicitly given as application/x-www-form-urlencoded. Elements of a form may even hint a client on what type a property has, its admissible range in terms of numeric values or even allow a client to chose a certain date or time point through various widgets.

Of course this all depends on the capabilities of the media-type exchanged. I.e. application/json doesn't have these and as such exchanging a resource state in such a representation format will not be very helpful to a client unless it has built-in support for the data returned, which already indicates a tight coupling to the service. As Evert already mentioned hypertext application language (HAL), this JSON based media-type allows servers to teach clients of what URIs are, plain JSON does not have that concept i.e., and allows URIs to be attached to link-relation names. It is therefore a good generic media type for describing general-purpose resources to clients. It though lacks capabilities of teaching clients on how to make form-based requests. Luckily there are either other media-types such as Ion (Amazon Ion) or extensions of HAL, i.e. HAL forms available that close that gap.

So, while inf3rno has given an example of how you might construct links in your response, the actual representation generated depends on the negotiated media type actually.

In HAL your JSON payload could be represented like this:

{
    "nome": "Porto",
    "_links": {
        "self": {
            "href": "/municipios/Porto"
        },
        ...
    },
    "_embedded": {
        "freguesias": {
            "_links": {
                "self": {
                    "href": "/municipios/Porto/freguesias"
                },
                "https://.../rel/parent": {
                    "href": "/municipios/Porto"
                },
                "https://.../rel/freguesias/Bonfim": {
                    "href": "/municipios/Porto/freguesias/Bonfim"
                },
                "https://.../rel/freguesias/Campanha": {
                    "href": "/municipios/Porto/freguesias/Campanha"
                },
                ...
            },
            ...
        }
    }
}

In Ion the same resource state may look like this:

{
    "name": "Porto",
    "self": { "href": "/municipios/Porto" },
    "freguesias": {
        "href": "/municipios/Porto/freguesias",
        "value": [
            "https://.../rel/parent": { "href": "/municipios/Porto" },
            "https://.../rel/freguesias/Bonfim": { "href": "/municipios/Porto/freguesias/Bonfim" },
            "https://.../rel/freguesias/Campanha": { "href": "/municipios/Porto/freguesias/Campanha" },
            ...
        ]
    }
}

Which one you prefer depends on you. In general, the more different media types your applications are able to process the more likely they will be to interoperate with other peers in the network without requiring you to manually touch your application. I.e. you could add support for both HAL and ION and then let the client decide on what representation it prefers through means of content-negotiation.

  • Related