You are currently viewing Getting Started with Spring HATEOAS: A Beginner’s Tutorial

Getting Started with Spring HATEOAS: A Beginner’s Tutorial

Spring HATEOAS (Hypermedia as the Engine of Application State) is a key module in the Spring ecosystem that simplifies creating RESTful APIs by including hypermedia links in responses. It encourages building APIs that are more self-descriptive and navigable, following REST principles closely.

Here’s a guide to get you started with Spring HATEOAS, along with examples.

Prerequisites

  • Basic knowledge of Spring Boot
  • JDK 11 or higher
  • Maven or Gradle build tool
  • An IDE like IntelliJ IDEA or Eclipse

Step 1: Create a Spring Boot Project

You can start by generating a new Spring Boot project from Spring Initializr with the following dependencies:

  • Spring Web
  • Spring HATEOAS
  • Spring Data JPA (Optional: If you want to interact with a database)

Step 2: Add Dependencies

If you’re using Maven, add the following dependencies to your pom.xml:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter HATEOAS -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>

    <!-- Spring Boot Starter Data JPA (Optional) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- H2 Database (Optional) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

If you’re using Gradle, add the following dependencies to your build.gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-hateoas'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Optional
    runtimeOnly 'com.h2database:h2' // Optional
}

Step 3: Define a Simple Entity

Let’s assume you’re building a simple Employee API.

@Entity
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String role;

    // Constructors, Getters, and Setters
    public Employee() {}

    public Employee(String name, String role) {
        this.name = name;
        this.role = role;
    }

    // Getters and Setters
}

Step 4: Create a Repository

You need a repository to manage your entity. Spring Data JPA simplifies this.

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}

Step 5: Create a Resource Representation Model

To include hypermedia links in your response, you need a representation model.

import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;

public class EmployeeModelAssembler implements RepresentationModelAssembler<Employee, EntityModel<Employee>> {

    @Override
    public EntityModel<Employee> toModel(Employee employee) {
        return EntityModel.of(employee,
                WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getEmployeeById(employee.getId())).withSelfRel(),
                WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getAllEmployees()).withRel("employees"));
    }
}

Step 6: Create a REST Controller

Next, create a REST controller that handles the HTTP requests.

import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final EmployeeRepository repository;
    private final EmployeeModelAssembler assembler;

    public EmployeeController(EmployeeRepository repository, EmployeeModelAssembler assembler) {
        this.repository = repository;
        this.assembler = assembler;
    }

    // GET all employees
    @GetMapping
    public CollectionModel<EntityModel<Employee>> getAllEmployees() {
        List<EntityModel<Employee>> employees = repository.findAll().stream()
                .map(assembler::toModel)
                .collect(Collectors.toList());

        return CollectionModel.of(employees, 
                WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getAllEmployees()).withSelfRel());
    }

    // GET employee by ID
    @GetMapping("/{id}")
    public EntityModel<Employee> getEmployeeById(@PathVariable Long id) {
        Employee employee = repository.findById(id)
                .orElseThrow(() -> new EmployeeNotFoundException(id));

        return assembler.toModel(employee);
    }

    // POST new employee
    @PostMapping
    public ResponseEntity<?> createEmployee(@RequestBody Employee newEmployee) {
        Employee savedEmployee = repository.save(newEmployee);

        return ResponseEntity
                .created(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getEmployeeById(savedEmployee.getId())).toUri())
                .body(assembler.toModel(savedEmployee));
    }

    // PUT (update) employee
    @PutMapping("/{id}")
    public ResponseEntity<?> updateEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) {
        Employee updatedEmployee = repository.findById(id)
                .map(employee -> {
                    employee.setName(newEmployee.getName());
                    employee.setRole(newEmployee.getRole());
                    return repository.save(employee);
                })
                .orElseGet(() -> {
                    newEmployee.setId(id);
                    return repository.save(newEmployee);
                });

        return ResponseEntity.ok(assembler.toModel(updatedEmployee));
    }

    // DELETE employee
    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteEmployee(@PathVariable Long id) {
        repository.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

Step 7: Exception Handling

Handle exceptions gracefully, e.g., when an employee is not found.

@ResponseStatus(HttpStatus.NOT_FOUND)
public class EmployeeNotFoundException extends RuntimeException {
    public EmployeeNotFoundException(Long id) {
        super("Could not find employee " + id);
    }
}

Step 8: Test Your Application

Run your Spring Boot application and test it using a tool like Postman or cURL.

For example, to retrieve all employees:

curl -X GET http://localhost:8080/employees

The response will include hypermedia links:

{
  "_embedded": {
    "employeeList": [
      {
        "id": 1,
        "name": "John Doe",
        "role": "Developer",
        "_links": {
          "self": {
            "href": "http://localhost:8080/employees/1"
          },
          "employees": {
            "href": "http://localhost:8080/employees"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/employees"
    }
  }
}

Conclusion

Spring HATEOAS helps you build RESTful APIs that are self-descriptive and navigable, adhering to REST principles. By including hypermedia links in your responses, clients can discover available actions and resources without needing extensive documentation.

This example is a basic introduction to Spring HATEOAS, and you can expand it by exploring advanced features like custom links, pagination, and more complex resource assemblies.

Leave a Reply