Hypermedia Driven REST API With Spring HATEOAS

Joe • updated : August 17, 2020

A software application is said to be RESTful if the engine of the application state is driven by hypertext (hypermedia). which means that it allows an extensive cross referencing between related data and ensures further operations will depend on the state of the resource.

HATEOAS (Hypermedia As The Engine Of Application State) is a library of API’s for link creations and resource representation. This enables a client application to navigate a REST API with minimal understanding of the API’s URLs. Instead, it understands relationships between the resources served by the API and uses its understanding of those relationships to discover the API’s URLs as it traverses those relationships.

Spring HATEOAS offers a set of classes and resource assemblers that makes it easier to create links that point to Spring MVC controllers, build up resource representations, and control how they are rendered into supported hypermedia formats (such as HAL - Json Hypertext Application Language).

HAL is a lightweight mediatypethat allows encoding not just data but also hypermedia controls, alerting consumers to other parts of the API they can navigate toward.

API Domain Overview

Without diving deep into the biological meaning of Enzymes and Proteins, our resource classes comprise of EnzymeModel and ProteinModel. The EnzymeModel class has a one-to-many relationship with the ProteinModel class. Other attributes and relationships have been omitted for the purpose of this tutorial.

@Value
@Builder
@JsonPropertyOrder({"enzymeName", "ecNumber", "enzymeFamily","alternativeNames","catalyticActivities","cofactors","associatedProteins"})
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class EnzymeEntry implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "enzyme name", example = "Alcohol dehydrogenase")
    @JsonProperty("enzymeName")
    final String enzymeName;
    @Schema(description = "Enzyme Classfication (EC) number", example = "1.1.1.1", required = true)
    @NotBlank
    @EqualsAndHashCode.Include
    @JsonProperty("ecNumber")
    final String ecNumber; 
    @JsonProperty("enzymeFamily")
    private final String enzymeFamily;
    @JsonProperty("alternativeNames")
    private final Set<String> alternativeNames;
    @JsonProperty("catalyticActivities")
    private final List<String> catalyticActivities;
    @JsonProperty("cofactors")
    private final Set<String> cofactors;
    @JsonProperty("associatedProteins")
    private final List<ProteinGroupEntry> associatedProteins;
}

We will focus on 3 controller methods

  • GET /enzymes/ - find all enzymes
  • GET /enzymes/{ec} – find an enzyme by ec
  • GET /enzymes/{ec}/proteins – find proteins by ec
    • GET /protein/{accession} – find a protein by accession

API Index

{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/computingfacts/rest/"
    },
    "enzymes" : {
      "href" : "http://localhost:8080/computingfacts/rest/enzymes/?page=0&size=10"
    },
    "enzyme" : {
      "href" : "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1"
    },
    "associated proteins" : {
      "href" : "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1/proteins?limit=10"
    },
    "protein" : {
      "href" : "http://localhost:8080/computingfacts/rest/protein/P1234"
    }
  }
}

Without Spring HATEOAS, the output of the /enzyme/{ec}

curl -X GET "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1" -H "accept: application/json"

url that returns the EnzymeEntrydomain class will look like this



{
  "enzymeName": "Alcohol dehydrogenase",
  "ecNumber": "1.1.1.1",
  "enzymeFamily": "Oxidoreductases",
  "alternativeNames": [
    "Aldehyde reductase."
  ],
  "catalyticActivities": [
    "primary alcohol + NAD(+) = an aldehyde + H(+) + NADH"
  ],
  "cofactors": [
    "Zn(2+) or Fe cation."
  ],
  "associatedProteins": [
    {
      "accession": "E1ACQ9",
      "proteinName": "Alcohol dehydrogenase notN",
      "organismName": "Aspergillus sp. (strain MF297-2)",
    }
  ]
}

As we can see that this json result does not tell us if there are more information about the enzyme resource that we can traverse. However, a RESTful representation of the enzymeEntry (EnzymeModel) should look like this;

{
  "enzymeName": "Alcohol dehydrogenase",
  "ecNumber": "1.1.1.1",
  "enzymeFamily": "Oxidoreductases",
  "alternativeNames": [
    "Aldehyde reductase."
  ],
  "catalyticActivities": [
    "primary alcohol + NAD(+) = an aldehyde + H(+) + NADH"
  ],
  "cofactors": [
    "Zn(2+) or Fe cation."
  ],
  "associatedProteins": {
    "_embedded": {
      "proteins": [
        {
          "accession": "E1ACQ9",
          "proteinName": "Alcohol dehydrogenase notN",
          "organismName": "Aspergillus sp. (strain MF297-2)",
          "_links": {
            "protein": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9"
            },
            "protein structure": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/proteinStructure"
            },
            "reactions": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/reaction?limit=10"
            },
            "pathways": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/pathways"
            },
            "small molecules": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/smallmolecules"
            },
            "diseases": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/diseases"
            },
            "literature": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/citation?limit=10"
            }
          }
        },
        {
          "accession": "P9WQC6",
          "proteinName": "Alcohol dehydrogenase B",
          "organismName": "Mycobacterium tuberculosis (strain CDC 1551 / Oshkosh)",
          "_links": {
            "protein": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6"
            },
            "protein structure": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/proteinStructure"
            },
            "reactions": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/reaction?limit=10"
            },
            "pathways": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/pathways"
            },
            "small molecules": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/smallmolecules"
            },
            "diseases": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/diseases"
            },
            "literature": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/citation?limit=10"
            }
          }
        }
      ]
    }
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1"
    },
    "enzymes": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/?page=0&size=10"
    },
    "associated Proteins": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1/proteins?limit=10"
    }
  }
}

This result is formatted using HAL and shows links which a client can navigate to get additional information about the enzyme resource.

To enable hypermedia in our REST API, we need to add the Spring HATEOAS starter dependency to the build.

Spring-Boot starter Maven Dependency

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>

The dependency does not only add Spring HATEOAS to the project’s classpath, but also enables the required configurations. All we need to do next is to design our controllers to return resource types instead of domain types.

Adding Hyperlinks

Spring HATEOAS provides a set of classes (RepresentationModel, EntityModel, CollectionModeland PagedModel ) with convenient methods for adding links to our model-based objects.

First, we extend the RepresentationModel class in order to inherit all the methods for adding link(s) to our model.

@Value
@Builder
@Relation(collectionRelation = "enzymes", itemRelation = "enzymeModel")
@JsonPropertyOrder({"enzymeName", "ecNumber", "enzymeFamily","alternativeNames","catalyticActivities","cofactors","associatedProteins"})
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class EnzymeModel extends RepresentationModel<EnzymeModel> implements Serializable {

    private static final long serialVersionUID = 1L;

    @Schema(description = "enzyme name", example = "Alcohol dehydrogenase")
    @JsonProperty("enzymeName")
    final String enzymeName;
    @Schema(description = "Enzyme Classfication (EC) number", example = "1.1.1.1", required = true)
    @NotBlank
    @EqualsAndHashCode.Include
    @JsonProperty("ecNumber")
    final String ecNumber; //uniquely identifies an Enzyme
    @JsonProperty("enzymeFamily")
    private final String enzymeFamily;
    @JsonProperty("alternativeNames")
    private final Set<String> alternativeNames;
    @JsonProperty("catalyticActivities")
    private final List<String> catalyticActivities;
    @JsonProperty("cofactors")
    private final Set<String> cofactors;
    @JsonProperty("associatedProteins")
    private final CollectionModel<ProteinModel> associatedProteins;
    


}

Note – the annotation @Relation(collectionRelation = "enzymes", itemRelation = "enzymeModel") enables us to customise the name of our collection.

Spring MVC Link builder

While the inherited methods of the RepresentationModel class are convenient, but we are still limited to hardcoding and duplicating URI string all over the place.

To address that, Spring HATEOAS provides aWebMvcLinkBuilder class that enables us to build links relative to the base URL of our Spring MVC controllers. It uses the controller’s base path as the foundation of the Link object we are creating. We will be using the static methods methodOn(..) and linkTo(Class controller,…)to create new link(s) with a base of the mapping annotated to our controller class.

   import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;

    @GetMapping(value = "/ec{ec}")
    public ResponseEntity<EntityModel<EnzymeEntry>> findEnzymeByEc(@Parameter(description = "a valid EC number.", required = true) @PathVariable("ec") @Size(min = 7, max = 7) String ec) {

        return enzymeService.getEnzyme(ec)
                .map(enzyme -> toEntityModel(enzyme))
                .map(ResponseEntity::ok)
                .orElseThrow(() -> new ResourceNotFoundException(String.format("Enzyme with ec=%s not found", ec)));

    }

    private EntityModel<EnzymeEntry> toEntityModel(EnzymeEntry enzyme) {
        return EntityModel.of(enzyme,
                linkTo(methodOn(EnzymeController.class)
                        .findEnzymeByEcNumber(enzyme.getEc()))
                        .withSelfRel());

    }

As the mapping from an entity to a representation model must be used in multiple places and we will need to build links in multiple controller methods, it is a good practice to have a class dedicated for this purpose rather than duplicating code all over the place.

First, we implement the RepresentationModelAssembler interface in order to transform our non-model-based object (EnzymeEntry) to a model-based object (EnzymeModel).

@Component
public class EnzymeModelAssembler implements RepresentationModelAssembler<EnzymeEntry, EnzymeModel> {

    private static final int START = 0;
    private static final int LIMIT = 10;
    private static final String ENZYMES = "enzymes";
    private static final String ASSOCIATED_PROTEINS = "associated Proteins";

    private final ProteinModelAssembler proteinModelAssembler;

    public EnzymeModelAssembler(ProteinModelAssembler proteinModelAssembler) {
        this.proteinModelAssembler = proteinModelAssembler;
    }


    @Override
    public EnzymeModel toModel(EnzymeEntry enzyme) {
        EnzymeModel model = buildEnzymeModel(enzyme);
        Class<EnzymeController> controllerClass = EnzymeController.class;
        model.add(linkTo(methodOn(controllerClass).findEnzymeByEcNumber(enzyme.getEc())).withSelfRel());
        model.add(linkTo(methodOn(controllerClass).enzymes(START, LIMIT)).withRel(ENZYMES));
        model.add(linkTo(methodOn(controllerClass).findAssociatedProteinsByEcNumber(enzyme.getEc(), LIMIT)).withRel(ASSOCIATED_PROTEINS));

        return model;
    }

    private EnzymeModel buildEnzymeModel(EnzymeEntry enzyme) {
        return EnzymeModel.builder()
                .ecNumber(enzyme.getEc())
                .enzymeName(enzyme.getEnzymeName())
                .enzymeFamily(enzyme.getEnzymeFamily())
                .alternativeNames(enzyme.getAltNames())
                .catalyticActivities(enzyme.getCatalyticActivities())
                .cofactors(enzyme.getIntenzCofactors())
                .associatedProteins(proteinModelAssembler.toCollectionModel(enzyme.getProteinGroupEntry()))
                .build();
    }

}

The toModel(T entity) method is convenient for converting our entity to a model-based object and with the benefit of adding links to our model.

The toCollectionModel(Iterable entities) is also available for converting collection of entities and it returns a model-based enzyme resources (CollectionModel<>).

CollectionModel<T>is Spring HATEOAS container for encapsulating collections of resources.

We can now use the EnzymeModelAssembler class in our controllers.

    private final EnzymeModelAssembler enzymeModelAssembler;
    private final EnzymeService enzymeService;

    public ResponseEntity<EnzymeModel> findEnzymeByEcNumber(@Parameter(description = "a valid EC number.", required = true) @PathVariable("ec") @Size(min = 7, max = 7) String ec) {
        validateEC(ec);
        return enzymeService.getEnzyme(ec)
                .map(enzyme -> enzymeModelAssembler.toModel(enzyme))
                .map(ResponseEntity::ok)
                .orElseThrow(() -> new ResourceNotFoundException(String.format("Enzyme with ec=%s not found", ec)));

    }

Similar to implementing the RepresentationModelAssembler interface, we can also transform the domain class (ProteinGroupEntry) to a model-based class (ProteinModel) by extending the RepresentationModelAssemblerSupport class.

public class ProteinModelAssembler extends RepresentationModelAssemblerSupport<ProteinGroupEntry, ProteinModel> {

    private static final int LIMIT = 10;
    private static final String PROTEIN_STRUCTURE = "protein structure";
    private static final String REACTIONS = "reactions";
    private static final String PATHWAYS = "pathways";
    private static final String SMALL_MOLECULES = "small molecules";
    private static final String DISEASES = "diseases";
    private static final String LITERATURE = "literature";
    private static final String PROTEIN = "protein";

    public ProteinModelAssembler(Class<?> controllerClass, Class<ProteinModel> resourceType) {
        super(controllerClass, resourceType);
    }

    @Override
    public ProteinModel toModel(ProteinGroupEntry protein) {
        ProteinModel model = ProteinUtil.toProteinModel(protein);
        Class<ProteinController> controllerClass = ProteinController.class;
        model.add(linkTo(methodOn(controllerClass).findProteinByAccession(protein.getPrimaryAccession())).withRel(PROTEIN));
        model.add(linkTo(methodOn(controllerClass).findProteinStructureByAccession(protein.getPrimaryAccession())).withRel(PROTEIN_STRUCTURE));
        model.add(linkTo(methodOn(controllerClass).findReactionsByAccession(protein.getPrimaryAccession(), LIMIT)).withRel(REACTIONS));
        model.add(linkTo(methodOn(controllerClass).findPathwaysByAccession(protein.getPrimaryAccession())).withRel(PATHWAYS));
        model.add(linkTo(methodOn(controllerClass).findSmallmoleculesByAccession(protein.getPrimaryAccession())).withRel(SMALL_MOLECULES));
        model.add(linkTo(methodOn(controllerClass).findDiseasesByAccession(protein.getPrimaryAccession())).withRel(DISEASES));
        model.add(linkTo(methodOn(controllerClass).findCitationsByAccession(protein.getPrimaryAccession(), LIMIT)).withRel(LITERATURE));
        return model;

    }

    @Override
    public CollectionModel<ProteinModel> toCollectionModel(Iterable<? extends ProteinGroupEntry> entries) {
        return StreamSupport
                .stream(entries.spliterator(), false)
                .map(this::toModel)
                .collect(Collectors.collectingAndThen(Collectors.toList(), CollectionModel::of));

    }

}

Next, create a @bean of the RepresentationModelAssemblerSupport

    @Bean
    public ProteinModelAssembler proteinModelAssembler() {
        return new ProteinModelAssembler(ProteinController.class, ProteinModel.class);
    }

TheproteinModelAssembler bean is now ready to be used in the controller method.

    private final EnzymeService enzymeService;
    private final ProteinModelAssembler proteinModelAssembler;

    @GetMapping(value = "/{ec}/proteins")
    public ResponseEntity<CollectionModel<ProteinModel>> findAssociatedProteinsByEcNumber(@Parameter(description = "a valid EC number.", required = true) @PathVariable("ec") String ec, @Parameter(description = " result limit") @RequestParam(value = "limit", defaultValue = "10") int limit) {
        Link selfLink = linkTo(methodOn(EnzymeController.class).findAssociatedProteinsByEcNumber(ec, limit)).withSelfRel();
        return ResponseEntity.ok(proteinModelAssembler.toCollectionModel(enzymeService.getProteinsByEc(ec, limit)).add(selfLink));
    }

ProteinModel HAL output

{
  "_embedded": {
    "proteins": [
      {
        "accession": "D7VTB4",
        "proteinName": "Putative phosphonate catabolism associated alcohol dehydrogenase",
        "organismName": "Sphingobacterium spiritivorum ATCC 33861",
        "_links": {
          "protein": {
            "href": "http://localhost:8080/computingfacts/rest/protein/D7VTB4"
          },
          "protein structure": {
            "href": "http://localhost:8080/computingfacts/rest/protein/D7VTB4/proteinStructure"
          },
          "reactions": {
            "href": "http://localhost:8080/computingfacts/rest/protein/D7VTB4/reaction?limit=10"
          },
          "pathways": {
            "href": "http://localhost:8080/computingfacts/rest/protein/D7VTB4/pathways"
          },
          "small molecules": {
            "href": "http://localhost:8080/computingfacts/rest/protein/D7VTB4/smallmolecules"
          },
          "diseases": {
            "href": "http://localhost:8080/computingfacts/rest/protein/D7VTB4/diseases"
          },
          "literature": {
            "href": "http://localhost:8080/computingfacts/rest/protein/D7VTB4/citation?limit=10"
          }
        }
      },
      {
        "accession": "C0QM54",
        "proteinName": "Zn-dependet alcohol dehydrogenase",
        "organismName": "Desulfobacterium autotrophicum (strain ATCC 43914 / DSM 3382 / HRM2)",
        "_links": {
          "protein": {
            "href": "http://localhost:8080/computingfacts/rest/protein/C0QM54"
          },
          "protein structure": {
            "href": "http://localhost:8080/computingfacts/rest/protein/C0QM54/proteinStructure"
          },
          "reactions": {
            "href": "http://localhost:8080/computingfacts/rest/protein/C0QM54/reaction?limit=10"
          },
          "pathways": {
            "href": "http://localhost:8080/computingfacts/rest/protein/C0QM54/pathways"
          },
          "small molecules": {
            "href": "http://localhost:8080/computingfacts/rest/protein/C0QM54/smallmolecules"
          },
          "diseases": {
            "href": "http://localhost:8080/computingfacts/rest/protein/C0QM54/diseases"
          },
          "literature": {
            "href": "http://localhost:8080/computingfacts/rest/protein/C0QM54/citation?limit=10"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1/proteins?limit=2"
    }
  }
}

The  "proteins" collection is listed underneath the  "_embedded "  section. This is how HAL represents collections.

Adding Pagination Links to API Resources

The same principle applies but we will need to either implement the RepresentationModelAssembler, PagedModel>> interface or use the org.springframework.data.web. PagedResourcesAssembler class which is an implementation of the aforementioned interface.The example code on how to achieve this is discussed here.

Next, we will look at how to test our RESTful API using Traverson.

The example code is availabe on GitHub. Happy Coding!!!

Reference:

Spring HATEOAS - https://docs.spring.io/spring-hateoas/docs/current/reference/html/

Similar Posts ..

Subscribe to our monthly newsletter. No spam, we promise !

Guest