Java REST Swagger vs. HATEOAS
In modern RESTful API development, API documentation and hypermedia-driven APIs play a crucial role. Two key concepts that developers often use are Swagger and HATEOAS. While Swagger helps document APIs, HATEOAS enhances API discoverability by including hypermedia links. Let us delve into understanding Swagger vs HATEOAS in Java REST.
1. Understanding Swagger and HATEOAS
1.1 What is Swagger?
Swagger, now officially known as OpenAPI, is a framework used for API documentation, design, and testing. It provides a structured way to describe RESTful APIs using a machine-readable format, typically in JSON or YAML. Swagger offers several key components:
- OpenAPI Specification (OAS) – A standardized format to describe API endpoints, parameters, responses, and authentication.
- Swagger UI – A web-based interactive UI that allows developers to explore and test API endpoints.
- Swagger Editor – A browser-based editor to design and document APIs using OpenAPI.
- Swagger Codegen – A tool to generate client SDKs and server stubs in multiple programming languages based on OpenAPI specs.
By providing interactive API documentation, Swagger simplifies API consumption for developers, testers, and consumers.
1.1.1 What is swagger.yml file?
The swagger.yml
file is a YAML-based OpenAPI specification that documents the Spring Boot CRUD API. It defines the API endpoints, HTTP methods, request and response structures, and associated parameters. The file includes metadata such as the API title, description, version, and server URL. Each endpoint is described with its respective path, method (GET, POST, DELETE), expected request body or query parameters, and response codes with corresponding descriptions. The schema section defines the structure of application entities, specifying field names, data types, and constraints.
Additionally, it integrates HATEOAS support by documenting links to related operations. This file ensures API consistency, improves developer understanding, and enables the automatic generation of interactive API documentation via Swagger UI.
1.2 What is HATEOAS?
HATEOAS (Hypermedia as the Engine of Application State) is a RESTful API principle that enhances API discoverability by embedding hypermedia links within responses. This approach allows clients to navigate and interact with an API dynamically without prior knowledge of its structure.
1.2.1 Key aspects of HATEOAS
- Hypermedia-driven – API responses include hyperlinks to related resources, guiding clients on available actions.
- Self-descriptive – Clients understand available operations dynamically from the response itself.
- Reduces client-side coupling – Changes in API structure do not require modifications on the client side, making APIs more adaptable.
1.2.2 Common Hypermedia Formats
- HAL (Hypertext Application Language) – Uses JSON or XML to structure hypermedia links.
- Siren – A richer format supporting actions and entities.
- JSON-LD – A linked data format using JSON to structure API responses.
For example, a HATEOAS-compliant API response for a user resource might look like this:
1 | {"id":123,"name":"John Doe","email":"john.doe@example.com","links":[{"rel":"self","href":"https://api.example.com/users/123"},{"rel":"orders","href":"https://api.example.com/users/123/orders"}]} |
1.3 Key Differences between Swagger and HATEOAS
Aspect | Swagger | HATEOAS |
---|---|---|
Purpose | API documentation & testing | Enhances API discoverability with links |
Format | JSON/YAML | Hypermedia (HAL, Siren, etc.) |
Client Guidance | Predefined documentation | Dynamic link-driven interactions |
Usage | Used for designing, documenting, and generating API client/server code | Used within APIs to provide dynamic navigation |
API Evolution | Changes require updating documentation and client implementation | Clients adapt dynamically based on response links |
2. Code Example
Visit Spring Initializr and generate the application required for this article.
2.1 Adding Project Dependencies
Include the following dependencies in the pom.xml
file.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <!-- Dependency for building a Spring Boot web application --> < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > </ dependency > <!-- Dependency for Spring Data JPA to work with databases --> < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-data-jpa</ artifactId > </ dependency > <!-- H2 Database dependency for an in-memory database --> < dependency > < groupId >com.h2database</ groupId > < artifactId >h2</ artifactId > </ dependency > <!-- SpringDoc OpenAPI dependency for Swagger UI documentation --> < dependency > < groupId >org.springdoc</ groupId > < artifactId >springdoc-openapi-starter-webmvc-ui</ artifactId > < version >2.0.2</ version > </ dependency > <!-- Spring Boot HATEOAS dependency for hypermedia-driven REST APIs --> < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-hateoas</ artifactId > </ dependency > |
2.2 Entity Class
Create an entity model class to interact with the database repository. We will use Lombok to simplify the generation of getters and setters.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package com.example.model; import jakarta.persistence.*; import lombok.Data; @Entity @Table (name = "product_table" ) @Data public class Product { @Id @GeneratedValue (strategy = GenerationType.AUTO) private Long id; private String name; private double price; public Product() { } public Product(String name, double v) { this .name = name; this .price = v; } // Getters and Setters. } |
2.3 Repository Class
Create a repository class responsible for interacting with the database and managing CRUD operations.
1 2 3 4 5 6 7 8 9 | package com.example.repository; import com.example.model.Product; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface ProductRepository extends JpaRepository<Product, Long> { } |
2.4 Controller Class
Create a controller class to handle client requests, communicate with the database, and return responses enriched with Atom links, commonly known as HATEOAS. This class will demonstrate all CRUD operations except the PUT method, which is intentionally left out for brevity, allowing learners to implement it themselves. Additionally, ensure that any new method added to the controller class is also mapped in the swagger.yml
file.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | package com.example.controller; import com.example.model.Product; import com.example.repository.ProductRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.stream.Collectors; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @RestController @Slf4j public class ProductController { private final ProductRepository repository; public ProductController(ProductRepository repository) { this .repository = repository; } // GET endpoint to retrieve all products with HATEOAS links @GetMapping ( "/products" ) public CollectionModel<EntityModel<Product>> getAllProducts() { log.info( "Getting all products" ); List<Product> products = repository.findAll(); // Convert each product into an EntityModel with HATEOAS links List<EntityModel<Product>> models = products.stream() .map(product -> EntityModel.of(product, linkTo(methodOn(ProductController. class ).getProductById(product.getId())).withSelfRel(), linkTo(methodOn(ProductController. class ).deleteProduct(product.getId())).withRel( "delete-product" ) )) .collect(Collectors.toList()); // Return the collection of products with a self link return CollectionModel.of(models, linkTo(methodOn(ProductController. class ).getAllProducts()).withSelfRel()); } // GET endpoint to retrieve a single product by ID with HATEOAS links @GetMapping ( "/product/{id}" ) public EntityModel<Product> getProductById( @PathVariable Long id) { log.info( "Get product by id= " + id); // Find the product by ID or throw an exception if not found Product product = repository.findById(id).orElseThrow( () -> new RuntimeException( "Product Not found" )); // Return the product as an EntityModel with HATEOAS links return EntityModel.of(product, linkTo(methodOn(ProductController. class ).getProductById(id)).withSelfRel(), linkTo(methodOn(ProductController. class ).deleteProduct(id)).withRel( "delete-product" ), linkTo(methodOn(ProductController. class ).getAllProducts()).withRel( "all-products" ) ); } // POST endpoint to create a new product @PostMapping ( "/products" ) public ResponseEntity<EntityModel<Product>> createProduct( @RequestBody Product product) { // Save the new product to the database Product savedProduct = repository.save(product); // Return the saved product with its URI location return ResponseEntity.created(linkTo(methodOn(ProductController. class ).getProductById(savedProduct.getId())).toUri()) .body(EntityModel.of(savedProduct)); } // DELETE endpoint to remove a product by ID @DeleteMapping ( "/product/{id}" ) public ResponseEntity<Void> deleteProduct( @PathVariable Long id) { // Delete the product from the database repository.deleteById(id); // Return a 204 No Content response return ResponseEntity.noContent().build(); } } |
2.4.1 Code Explanation
The ProductController
class is a Spring Boot REST controller that manages product-related API endpoints. It uses Lombok’s @Slf4j
for logging and interacts with the ProductRepository
to perform CRUD operations. The constructor initializes the repository.
The @GetMapping("/products")
method retrieves all products, converts them into EntityModel
objects with HATEOAS links, and returns them as a CollectionModel
.
The @GetMapping("/product/{id}")
method fetches a single product by ID, adding HATEOAS links for self, delete, and all-products endpoints.
The @PostMapping("/products")
method saves a new product and returns a response with a Location header pointing to the newly created resource.
The @DeleteMapping("/product/{id}")
method removes a product by ID and returns a 204 No Content
response.
Each endpoint logs relevant information and follows RESTful best practices with HATEOAS support for enhanced API navigation.
2.5 Swagger YAML
Create a swagger.yml
file to document the Spring Boot CRUD API for managing products. The file should be located in the src/main/resources/static
directory.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | openapi: 3.0.1 info: title: Product API description: RESTful API for managing products with Swagger and HATEOAS. version: 1.0.0 servers: - url: http://localhost:10090 description: Local Development Server paths: /products: get: summary: Get all products operationId: getAllProducts tags: - Product responses: "200": description: List of products content: application/json: schema: type: array items: $ref: "#/components/schemas/Product" post: summary: Create a new product operationId: createProduct tags: - Product requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Product" responses: "201": description: Product created successfully content: application/json: schema: $ref: "#/components/schemas/Product" "/product/{id}": get: summary: Get product by ID operationId: getProductById tags: - Product parameters: - name: id in: path required: true schema: type: number responses: "200": description: Product details content: application/json: schema: $ref: "#/components/schemas/Product" "404": description: Product not found delete: summary: Delete product by ID operationId: deleteProduct tags: - Product parameters: - name: id in: path required: true schema: type: integer responses: "204": description: Product deleted successfully "404": description: Product not found components: schemas: Product: type: object properties: id: type: number example: 1 name: type: string example: Laptop price: type: number format: double example: 799.99 |
This OpenAPI 3.0.1 specification defines a Product API designed to manage products using Swagger and HATEOAS principles. The API runs on a local development server at http://localhost:10090
and supports multiple endpoints for interacting with product data.
The /products
endpoint supports a GET request to retrieve a list of products, returning an array of Product
objects, and a POST request to create a new product, requiring a JSON request body. The /product/{id}
endpoint allows fetching a specific product by ID via a GET request, returning a 404
response if the product is not found, and deleting a product using a DELETE request.
The API follows a structured response format in JSON, ensuring consistency and usability. The Product
schema includes three properties: id
(numeric), name
(string), and price
(double). API consumers can use this specification to integrate or test the Product API efficiently.
Keep in mind that if you are using Swagger annotations directly within the controller class, there is no need for the swagger.yml
file. However, while annotations work well for small to medium-sized projects, using a YAML file is a better approach for large-scale microservices projects, as it allows API documentation to be managed separately.
2.6 Application properties
Add the following properties to the src/main/resources/application.properties
file.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | spring.application.name=springboot-hateoas-swagger server.port=10090 # enable h2 spring.h2.console.enabled=true spring.h2.console.path=/h2-console # database configuration spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update # hibernate Settings spring.jpa.show-sql=true ## format SQL for better readability spring.jpa.properties.hibernate.format_sql=true spring.jpa.defer-datasource-initialization=true # enable query parameters in logs logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.orm.jdbc.bind=TRACE # swagger ui customization (springdoc openapi) springdoc.api-docs.path=/v3/api-docs springdoc.swagger-ui.path=/swagger-ui.html springdoc.api-docs.enabled=true springdoc.swagger-ui.url=/swagger.yml |
2.6.1 Properties Explanation
- Application Configuration:
spring.application.name=springboot-hateoas-swagger
– Sets the application name asspringboot-hateoas-swagger
.server.port=10090
– Configures the server to run on port10090
. You’re free to change the port number as per your needs.
- H2 Database Console:
spring.h2.console.enabled=true
– Enables the H2 database console for web access.spring.h2.console.path=/h2-console
– Sets the H2 console path to/h2-console
.
- Database Configuration:
spring.datasource.url=jdbc:h2:mem:testdb
– Configures an in-memory H2 database namedtestdb
.spring.datasource.driverClassName=org.h2.Driver
– Specifies the H2 database driver.spring.datasource.username=sa
– Sets the database username assa
(default for H2).spring.datasource.password=
– No password is set for the database.spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
– Configures Hibernate to use the H2 dialect.spring.jpa.hibernate.ddl-auto=update
– Automatically updates the database schema.
- Hibernate Settings:
spring.jpa.show-sql=true
– Enables logging of SQL queries.spring.jpa.properties.hibernate.format_sql=true
– Formats SQL queries for better readability in logs.spring.jpa.defer-datasource-initialization=true
– Ensures database initialization is deferred until Hibernate is fully set up.
- Logging Configuration:
logging.level.org.hibernate.SQL=DEBUG
– Logs SQL queries at the DEBUG level.logging.level.org.hibernate.orm.jdbc.bind=TRACE
– Logs SQL query parameters for better debugging.
- Swagger (SpringDoc OpenAPI) Configuration:
springdoc.api-docs.path=/v3/api-docs
– Specifies the path for OpenAPI documentation.springdoc.swagger-ui.path=/swagger-ui.html
– Sets the Swagger UI access path.springdoc.api-docs.enabled=true
– Enables OpenAPI documentation generation.springdoc.swagger-ui.url=/swagger.yml
– Configures Swagger UI to load documentation fromswagger.yml
.
2.7 Main Class
Create a main class that initializes and starts the application. This class will implement CommandLineRunner
to populate the table with mock data when the application launches.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | package com.example; import com.example.model.Product; import com.example.repository.ProductRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import java.util.List; import java.util.Random; // Swagger ui- http://localhost:10090/swagger-ui.html @SpringBootApplication @Slf4j public class SpringbootHateoasSwaggerApplication { public static void main(String[] args) { SpringApplication.run(SpringbootHateoasSwaggerApplication. class , args); log.info( "Application started successfully." ); } @Bean CommandLineRunner loadData(ProductRepository repository) { return args -> { if (repository.count() == 0 ) { List<String> productNames = List.of( "Laptop" , "Smartphone" , "Tablet" , "Smartwatch" , "Headphones" ); Random random = new Random(); int count = 0 ; for (String name : productNames) { double price = 50 + (random.nextDouble() * 950 ); // Random price between 50 and 1000 repository.save( new Product(name, Math.round(price * 100.0 ) / 100.0 )); // Round to 2 decimal places count = count + 1 ; } System.out.println(count + " random products inserted into the database." ); } else { System.out.println( "Skipping..." ); } }; } } |
2.8 Application Run
Launch the Spring Boot application and navigate to http://localhost:10090/swagger-ui.html to access the Swagger UI. Here, users can interact with various APIs, view responses, and explore HATEOAS links for better understanding.
Users can access the Swagger UI, but here is how the API response appears with HATEOAS:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | -- API request: Get all products curl -X 'GET' \ -H 'accept: application/json' -- API response { "_embedded": { "productList": [ { "id": 1, "name": "Laptop", "price": 812.17, "_links": { "self": { "href": "http://localhost:10090/product/1" }, "delete-product": { "href": "http://localhost:10090/product/1" } } }, { "id": 2, "name": "Smartphone", "price": 222.71, "_links": { "self": { "href": "http://localhost:10090/product/2" }, "delete-product": { "href": "http://localhost:10090/product/2" } } }, { "id": 3, "name": "Tablet", "price": 664.89, "_links": { "self": { "href": "http://localhost:10090/product/3" }, "delete-product": { "href": "http://localhost:10090/product/3" } } }, { "id": 4, "name": "Smartwatch", "price": 723.2, "_links": { "self": { "href": "http://localhost:10090/product/4" }, "delete-product": { "href": "http://localhost:10090/product/4" } } }, { "id": 5, "name": "Headphones", "price": 230.93, "_links": { "self": { "href": "http://localhost:10090/product/5" }, "delete-product": { "href": "http://localhost:10090/product/5" } } } ] }, "_links": { "self": { "href": "http://localhost:10090/products" } } } |
This API response follows the HATEOAS (Hypermedia as the Engine of Application State) principle, which enhances RESTful APIs by providing navigable links for resources. The response contains a list of products under the _embedded
key, each with details such as id
, name
, and price
. Each product includes an _links
section, which provides hypermedia controls, allowing clients to interact with the API dynamically. For example, the self
link points to the specific product’s URL (e.g., http://localhost:10090/product/1
), while the delete-product
link allows deletion of the product. Additionally, at the root level, a _links
section provides a self
link (http://localhost:10090/products
) for retrieving the entire product list. These links enable seamless navigation and interaction without hardcoding endpoint URLs, making the API more discoverable and maintainable.
3. Conclusion
Both Swagger and HATEOAS serve different purposes. Swagger simplifies API documentation and testing, whereas HATEOAS enhances API discoverability. By implementing both, developers can create well-documented and flexible REST APIs.