Aleksy
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Hypermedia API with Spring HATEOAS Did you ever consider the quality of your REST API? Did you know there are several levels of REST API? Did you ever heard the term HATEOAS? Or maybe you ever wonder how to implement it in Java? If so then in this articule we'll cover anwsers to that questions with main emphasis on HATEOAS concept and implementation of that concept with Spring HATEOAS project. ## What is HATEOAS? **H**ypermedia **A**s **T**he **E**ngine **O**f **A**pplication **S**tate - is one of the constraints of the REST architecture. Neither REST nor HATEOAS is any requirement or specification. How you implement it depends only on you. At this point, you may ask yourself - how RESTful your api is without using HATEOAS? This question is answered by the REST maturity model presented by Leonard Richardson. This model consists of four levels, as set out below: * level 0 - the API implementation uses the HTTP protocol, but not using it's full capabilites, additionally, unique addresses for resources are not provided. * level 1 - we have a unique identifier for the resource, but each action on the resource has its own url * level 2 - we use HTTP methods instead of verbs describing actions, e.g. DELETE method instead of url `.../delete` * level 3 - the term HATEOAS has been introduced. Simply speaking it introduces hypermedia to resources. This allows you to place links in the response informing about possible actions, thereby adding possibility to navigate through API. ![](https://i.imgur.com/jLznAex.png) Most projects are currently written using level 2. If we would like to go for the perfect RESTful API, we should consider HATEOAS. ![](https://i.imgur.com/TgOW3HG.png) Above is an example of a response from the server in the form of JSON+HAL. Such a resource consists of two parts: our data and links to actions that are possible to be performed on a given resource. ## SPRING HATEOAS 1.x.x You may be asking yourself how to implement HATEOAS in Java? You can of course write your solution, but why reinvent the wheel? The right tool for this seems to be the Spring Hateoas project. It is a long-standing solution on the market because its origins date back to 2012, but in 2019 we had a version 1.0 release. Of course, this version introduced a few changes compared to 0.x. They will be discussed at the end of the article after presenting some examples of using this library so that you better understand what the differences between the two versions are. Let's discuss the possibilities of the library based on a simple API that returns us a list of movies and related directors. Our domain looks like this: ```java @Entity public class Movie { @Id @GeneratedValue private Long id; private String title; private int year; private Rating rating; @ManyToOne private Director director; } @Entity public class Director { @Id @GeneratedValue @Getter private Long id; @Getter private String firstname; @Getter private String lastname; @Getter private int year; @OneToMany(mappedBy = "director") private Set<Movie> movies; } ``` We can approach to the implementation of HATEOAS in several ways. The three methods represented here are ranked from least to most recommended. But first we need to add some dependencies to our Spring Boot project: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> ``` Ok, now we can consider implementation options. ### 1. Entity extends RepresentationModel with links directly in Controller class Firstly extend our entity models with RepresentationModel. ```java public class Movie extends RepresentationModel<Movie> public class Director extends RepresentationModel<Director> ``` Then add links to RepresentationModel within each controller. The example below returns all directors from the system. By adding two links to each director - to himself and to the entire collection. A link is also added to the collection. The key elements of this code are two methods with static imports: * `linkTo()` - responsible for creating the link * `methodOn()` - enables to dynamically generate the path to a given resource. We don't need to hardcode the path but we can refer to the method in the controller ```java @GetMapping("/directors") public ResponseEntity<CollectionModel<Director>> getAllDirectors() { List<Director> directors = directorService.getAllDirectors(); directors.forEach(director -> { director.add(linkTo(methodOn(DirectorController.class).getDirectorById(director.getId())).withSelfRel()); director.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(director.getId())).withRel("directorMovies")); }); Link allDirectorsLink = linkTo(methodOn(DirectorController.class).getAllDirectors()).withSelfRel()); return ResponseEntity.ok(CollectionModel.of(directors, allDirectorsLink)); } ``` This is the response we get after invoking such controller: ![](https://i.imgur.com/1LpgZke.png) We can get similar result when requesting for a specific resource. ```java @GetMapping("/directors/{id}") public ResponseEntity<Director> getDirector(@PathVariable("id") Long id) { return directorService.getDirectorById(id) .map(director -> { director.add(linkTo(methodOn(DirectorController.class).getDirectorById(id)).withSelfRel()); director.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(id)).withRel("directorMovies")); director.add(linkTo(methodOn((DirectorController.class)).getAllDirectors()).withRel("directors")); return ResponseEntity.ok(director); }) .orElse(ResponseEntity.notFound().build()); } ``` ![](https://i.imgur.com/akS1APZ.png) The main advantage of this implementation is simplicity. But making our entity dependent on an external library is not a very good idea. Plus, the code repetition for adding links for a specific resource is immediately noticeable. You can of course bring it to some private method, but there is a better way. ### 2. Use Assemblers - SimpleRepresentationModelAssembler And it's not about assembly language, but about a special kind of class that convert our resource to RepresentationModel. One of such assemblers is SimpleRepresentationModelAssembler, its implementation is as follows: ```java @Component public class DirectorAssembler implements SimpleRepresentationModelAssembler<Director> { @Override public void addLinks(EntityModel<Director> resource) { Long directorId = resource.getContent().getId(); resource.add(linkTo(methodOn(DirectorController.class).getDirectorById(directorId)).withSelfRel()); resource.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(directorId)).withRel("directorMovies")); } @Override public void addLinks(CollectionModel<EntityModel<Director>> resources) { resources.add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withSelfRel()); } } ``` In this case, our entity will be wrapped in an `EnityModel` (this class extends `RepresentationModel`) to which the links specified by us in the `addLinks()` will be added. Here we overwrite two `addLinks()` methods - one for entire data collections and the other for single resources. Then, as part of the controller, it is enough to call the `toModel()` or `toCollectionModel()` method (`addLinks()` are template methods here), depending on whether we return a collection or a single representation. ```java @GetMapping public ResponseEntity<CollectionModel<EntityModel<Director>>> getAllDirectors() { return ResponseEntity.ok(directorAssembler.toCollectionModel(directorService.getAllDirectors())); } @GetMapping(value = "directors/{id}") public ResponseEntity<EntityModel<Director>> getDirectorById(@PathVariable("id") Long id) { return directorService.getDirectorById(id) .map(director -> { EntityModel<Director> directorRepresentation = directorAssembler.toModel(director) .add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withRel("directors")); return ResponseEntity.ok(directorRepresentation); }) .orElse(ResponseEntity.notFound().build()); } ``` The main benefit of using the `SimpleRepresentationModelAssembler` is the separation of our entity from the `RepresentationModel`, as well as the separation of the adding link logic from the controller. The problem arises when we want to add hypermedia to nested elements of an object. Obtaining the effect as in the example below is impossible with current way. ```json { "id": "M0002", "title": "Once Upon a Time in America", "year": 1984, "rating": "R", "directors": [ { "id": "D0001", "firstname": "Sergio", "lastname": "Leone", "year": 1929, "_links": { "self": { "href": "http://localhost:8080/directors/D0001" } } } ], "_links": { "self": { "href": "http://localhost:8080/movies/M0002" } } } ``` ### 3. Create DTO class with RepresentationModelAssembler The solution to this problem is to combine the two previous methods, modifying them slightly. In our opinion, `RepresentationModelAssembler` offers the most possibilities. It removes the restrictions that arose in the case of nested elements for `SimpleRepresentationModelAssembler`. But it also requires more code from us because we need to prepare a DTOs, which are often done anyway. This is the implementation based on `RepresentationModelAssembler`: ```java @Component public class DirectorRepresentationAssembler implements RepresentationModelAssembler<Director, DirectorRepresentation> { @Override public DirectorRepresentation toModel(Director entity) { DirectorRepresentation directorRepresentation = DirectorRepresentation.builder() .id(entity.getId()) .firstname(entity.getFirstname()) .lastname(entity.getLastname()) .year(entity.getYear()) .build(); directorRepresentation.add(linkTo(methodOn(DirectorController.class).getDirectorById(directorRepresentation.getId())).withSelfRel()); directorRepresentation.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(directorRepresentation.getId())).withRel("directorMovies")); return directorRepresentation; } @Override public CollectionModel<DirectorRepresentation> toCollectionModel(Iterable<? extends Director> entities) { CollectionModel<DirectorRepresentation> directorRepresentations = RepresentationModelAssembler.super.toCollectionModel(entities); directorRepresentations.add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withSelfRel()); return directorRepresentations; } } ``` When it comes to controller methods, they look the same as for `SimpleRepresentationModelAssembler`, the only difference is that in the `ResponseEntity` the return type is DTO - `DirectorRepresentation`. ```java @GetMapping public ResponseEntity<CollectionModel<DirectorRepresentation>> getAllDirectors() { return ResponseEntity.ok(directorRepresentationAssembler.toCollectionModel(directorService.getAllDirectors())); } @GetMapping(value = "/{id}") public ResponseEntity<DirectorRepresentation> getDirectorById(@PathVariable("id") String id) { return directorService.getDirectorById(id) .map(director -> { DirectorRepresentation directorRepresentation = directorRepresentationAssembler.toModel(director) .add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withRel("directors")); return ResponseEntity.ok(directorRepresentation); }) .orElse(ResponseEntity.notFound().build()); } ``` Here is our DTO model: ```java @Builder @Getter @EqualsAndHashCode(callSuper = false) @Relation(itemRelation = "director", collectionRelation = "directors") public class DirectorRepresentation extends RepresentationModel<DirectorRepresentation> { private final String id; private final String firstname; private final String lastname; private final int year; } ``` The `@Relation` annotation allows you to configure the relationship names to be used in the HAL representation. Without it, the relationship names match the class name and a suffix List for the collection. By default JSON+HAL looks like this ```json { "_embedded": { "directorRepresentationList": [ … ] }, "_links": { … } } ``` Hovever annotation @Relation can change name for `directors` ```json { "_embedded": { "directors": [ … ] }, "_links": { … } } ``` Summarizing the HATEOAS concept it consists of few pros and cons. #### Pros: * If the client uses it, we can change the API address for our resources without breaking the client * Creates good self documentation, and table of contents of API to person who have first contatct with our API * Can simplify building some conditions on frontend, e.g. whether the button should be disabled / enabled based on whether the link to corresponding the action exists * Less coupling between frontend and backend * Just like writing tests imposes on us to stick to the SRP principle in class construction, hateoas can keep us in check when designing API #### Cons: * Additional work needed on implementing non-business functionality * Additional network overhead. The size of the transferred data is larger * Adding links to some resources can be sometimes complicated and can introtuce mess in controllers ## Changes in Spring HATEOAS 1.0 Spring HATEOAS has been available since 2012, but first release of version 1.0 was in 2019. The main changes concerned the changes to the package paths and names of some classes, e.g. | Old | New | | -------- | -------- | | ResourceSupport | RepresentationModel | | Resource | EntityModel | | Resources | CollectionModel | | PagedResources | PagedModel | | ResourceAssembler | RepresentationModelAssembler | It is worth paying attention to a certain naming convention - the replacement of the word `Resource` in class names with the word `Representation`, this is because these types do not represent resources but representations, which can be enriched with hypermedia. It is also more in the spirit of REST. We are returning the resource representations, not the resources themselves. In the new version, there is a tendency to move away from constructors in favor of static construction methods - `.of()`. It is also worth mentioning that the old version has no equivalent for `SimpleRepresentationModelAssembler`. On the other hand, the `ResourceAssembler` interface has only the `toResource()`method (equivalent - `toModel()`) and no equivalent for `toCollectionModel()`. Such a method is found in `RepresentationModelAssembler` and is the` toModelCollection()` method. The creators of the library have also included a script that migrates old package paths and old class names to the new version. You can check it here https://docs.spring.io/spring-hateoas/docs/1.0.0.M1/reference/html/#migrate-to-1.0 ## Sources https://docs.spring.io/spring-hateoas/docs/1.0.0.M1/reference/html/ https://spring.io/guides/gs/rest-hateoas/ https://spring.io/guides/tutorials/rest/ https://github.com/spring-projects/spring-hateoas-examples https://martinfowler.com/articles/richardsonMaturityModel.html https://dzone.com/articles/applying-hateoas-to-a-rest-api-with-spring-boot https://spring.io/blog/2018/01/12/building-richer-hypermedia-with-spring-hateoas https://geek.justjoin.it/hateoas-api-moze-byc-lepsze https://www.baeldung.com/spring-hateoas-tutorial

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully