1. Overview
Two popular approaches to designing REST APIs are often used: Swagger and HATEOAS. Both aim to make APIs more user-friendly and understandable but follow distinct paradigms.
In this tutorial, we’ll see the difference between Swagger and HATEOAS and some common use cases.
2. What Is Swagger?
Swagger is a set of open-source tools for building, documenting, and consuming REST APIs. It allows developers to describe the structure of their APIs using a JSON or YAML file based on the OpenAPI Specification (OAS).
Let’s look at Swagger’s key features.
2.1. Code Generation
With Swagger, we can automatically generate interactive API documentation, code, and client libraries. Swagger can also create server stubs and client SDKs in various programming languages, speeding up development.
It’s an API-first approach that defines a contract between the requirements and the people maintaining the application.
Developers can use tools like SwaggerHub to create boilerplate code for different programming languages by providing a Swagger specification file. For example, let’s look at a YAML template for a simple User endpoint:
openapi: 3.0.1
info:
title: User API
version: "1.0.0"
description: API for managing users.
paths:
/users:
get:
summary: Get all users
security:
- bearerAuth: [] # Specifies security for this endpoint
responses:
'200':
description: A list of users.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized - Authentication required
'500':
description: Server error
post:
summary: Create a new user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewUser'
responses:
'201':
description: User created successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Invalid input
'401':
description: Unauthorized - Authentication required
'500':
description: Server error
/users/{id}:
get:
summary: Get user by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
example: 1
responses:
'200':
description: User found.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized - Authentication required
'404':
description: User not found
'500':
description: Server error
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT # JWT specifies the type of token expected
schemas:
User:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: John Doe
email:
type: string
example: [email protected]
createdAt:
type: string
format: date-time
example: "2023-01-01T12:00:00Z"
NewUser:
type: object
properties:
name:
type: string
example: John Doe
email:
type: string
example: [email protected]
required:
- name
- email
Let’s get an overview of the YAML file:
- General Information (info): The API title, version, and a brief description are included.
- Paths:
- GET /users: Retrieves all users, returning a 200 response with an array of User objects.
- POST /users: It creates a new user. It expects a request body with the NewUser schema and returns a 201 response with the created user object.
- GET /users/{id}: Retrieves a specific user by ID. Includes a 404 response if the User isn’t found
- Components:
- User schema: Defines the structure of a user object, including fields like id, name, email, and createdAt.
- NewUser schema: Used in the request body for creating a new user, requiring name and email fields.
- SecuritySchemes: This section defines how the API handles security. In this case, we specify a bearerAuth scheme, which uses Bearer tokens, often JWTs (JSON Web Tokens), in API security contexts.
We can define almost everything about an API and automatically generate it for the most common languages, speeding up this part of the process,
2.2. API Documentation
We can also directly apply the Open API documentation tags in our project’s code. Either with automatic generation or manual tagging, let’s look at how the user endpoint could look in a Java Spring REST application:
@RestController
@RequestMapping("/api/users")
public class UserController {
// fields and constructor
@Operation(summary = "Get all users", description = "Retrieve a list of all users")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "List of users",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "500", description = "Internal server error") })
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok()
.body(userRepository.getAllUsers());
}
@Operation(summary = "Create a new user", description = "Add a new user to the system")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "User created",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "400", description = "Invalid input") })
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> createUser(
@RequestBody(description = "User data", required = true,
content = @Content(schema = @Schema(implementation = NewUser.class))) NewUser user) {
return new ResponseEntity<>(userRepository.createUser(user), HttpStatus.CREATED);
}
@Operation(summary = "Get user by ID", description = "Retrieve a user by their unique ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User found",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "404", description = "User not found") })
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Integer id) {
return ResponseEntity.ok()
.body(userRepository.getUserById(id));
}
}
Let’s look at some of the most important annotations:
- @Operation: It adds a summary and description for each API operation, helping to describe what the endpoint does and what it’s for.
- @ApiResponse: It defines an individual response for an HTTP status code, including a description and the expected content type and schema.
- @Content: Specifies a response or request body’s content type (e.g., application/json) and provides the schema for data serialization.
- @Schema: Describes the data model for request and response bodies, associating classes (like User) with the JSON structure displayed in Swagger.
2.3. Interactive Console
The Swagger UI console is an interactive, web-based interface that dynamically generates documentation from OpenAPI specifications. It allows developers and API consumers to explore and test endpoints visually. The console displays API endpoints, request parameters, responses, and error codes organized in a user-friendly layout.
Each endpoint provides fields to input parameter values, headers, and request bodies, enabling users to make live requests directly from the console. This functionality helps developers understand API behavior, verify integrations, and troubleshoot issues without needing separate tools, making it an essential resource for API development and testing. For example, we can see a Swagger UI example for a pet store.
2.4. Benefit of an API First Approach
Why should we use a unique API contract or template for documentation?
A template ensures that all endpoints across the API follow a uniform structure. This consistency simplifies understanding and using the API, both for the internal development team and external consumers. For example, developers, QA engineers, and external stakeholders have a clear, shared understanding of the API’s capabilities and structure.
Furthermore, clients can experiment with the API directly within the documentation, making the API easier to adopt and integrate without needing extensive additional support. We can set up automated tests to ensure the API’s structure and responses meet specifications.
3. What Is HATEOAS?
HATEOAS (Hypermedia as the Engine of Application State) is a constraint of REST application architecture. It’s part of the broader REST paradigm and emphasizes that clients interact with a REST API entirely through hypermedia provided dynamically by the server. In HATEOAS, the server includes links within its responses, guiding the client on the next actions.
3.1. HATEOAS Example
Let’s look at a Spring HATEOAS application. First, we need to define our User as part of a specific representation model:
public class User extends RepresentationModel<User> {
private Integer id;
private String name;
private String email;
private LocalDateTime createdAt;
// Constructors, Getters, and Setters
}
Now, let’s see an example of how we can implement it for the user endpoint:
@RestController
@RequestMapping("/api/hateoas/users")
public class UserHateoasController {
// fields and constructor
@GetMapping
public CollectionModel<User> getAllUsers() {
List<User> users = userService.getAllUsers();
users.forEach(user -> {
user.add(linkTo(methodOn(UserController.class).getUserById(user.getId())).withSelfRel());
});
return CollectionModel.of(users, linkTo(methodOn(UserController.class).getAllUsers())
.withSelfRel());
}
@GetMapping("/{id}")
public EntityModel<User> getUserById(@PathVariable Integer id) {
User user = userService.getUserById(id);
user.add(linkTo(methodOn(UserController.class).getUserById(id)).withSelfRel());
user.add(linkTo(methodOn(UserController.class).getAllUsers()).withRel("all-users"));
return EntityModel.of(user);
}
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<EntityModel<User>> createUser(@RequestBody NewUser user) {
User createdUser = userService.createUser(user);
createdUser.add(
linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).withSelfRel());
return ResponseEntity.created(
linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).toUri())
.body(EntityModel.of(createdUser));
}
}
Let’s look at a sample response for the getAllUsers endpoint, where we can discover the User’s actions and related resources dynamically via the links:
[
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"createdAt": "2023-01-01T12:00:00",
"_links": {
"self": {
"href": "http://localhost:8080/users/1"
}
}
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"createdAt": "2023-02-01T12:00:00",
"_links": {
"self": {
"href": "http://localhost:8080/users/2"
}
}
}
]
3.2. Tests
To understand more in detail, let’s look at some integration tests for the controller.
Let’s start by getting all the users:
@Test
void whenAllUsersRequested_thenReturnAllUsersWithLinks() throws Exception {
User user1 = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
User user2 = new User(2, "Jane Smith", "[email protected]", LocalDateTime.now());
when(userService.getAllUsers()).thenReturn(List.of(user1, user2));
mockMvc.perform(get("/api/hateoas/users").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._embedded.userList[0].id").value(1))
.andExpect(jsonPath("$._embedded.userList[0].name").value("John Doe"))
.andExpect(jsonPath("$._embedded.userList[0]._links.self.href").exists())
.andExpect(jsonPath("$._embedded.userList[1].id").value(2))
.andExpect(jsonPath("$._embedded.userList[1].name").value("Jane Smith"))
.andExpect(jsonPath("$._links.self.href").exists());
}
In this case, we expect each User we retrieve to have a relative path by id.
Let’s also look at the endpoint to get the User by id:
@Test
void whenUserByIdRequested_thenReturnUserByIdWithLinks() throws Exception {
User user = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
when(userService.getUserById(1)).thenReturn(user);
mockMvc.perform(get("/api/hateoas/users/1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$._links.self.href").exists())
.andExpect(jsonPath("$._links.all-users.href").exists());
}
We now expect all users by id reference to exist in the response.
Finally, after we create a new user, we also expect the new reference to be in the response:
@Test
void whenUserCreationRequested_thenReturnUserByIdWithLinks() throws Exception {
User user = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
when(userService.createUser(any(NewUser.class))).thenReturn(user);
mockMvc.perform(post("/api/hateoas/users").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"John Doe\",\"email\":\"[email protected]\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$._links.self.href").exists());
}
3.3. Key Points
As we have seen, HATEOAS APIs include links in their responses, guiding the client’s actions. This reduces the need for the client to hard-code endpoint routes and enables a more flexible interaction with the API.
Likewise, it provides a way for the client to follow links provided by the server to navigate through various states or actions dynamically, enabling more adaptive workflows. Therefore, we can think of HATEOAS as the ultimate step in making our API explorable so the client can understand its behavior.
4. Key Differences Between Swagger and HATEOAS
Let’s illustrate the differences between Swagger and HATEOAS:
Aspect
Swagger
HATEOAS
API Documentation
Swagger provides detailed, human-readable API documentation with a UI, allowing consumers to understand available endpoints, request parameters, and responses in advance.
HATEOAS relies on hypermedia links returned by the server within responses, meaning that documentation is more implicit. Therefore, consumers discover actions dynamically through these links rather than a pre-generated UI.
Client-side Implementation
Clients are typically generated or written based on Swagger specifications. The API’s structure is known beforehand, and the client can make requests according to predefined paths.
HATEOAS clients interact with the API dynamically, discovering available actions through hypermedia links within responses. The client does not need to know the complete API structure in advance.
Flexibility
Swagger is more rigid, expecting predefined endpoints and a consistent API structure. This makes it harder to evolve the API without updating documentation or the spec.
HATEOAS offers more flexibility, allowing the API to evolve by changing the hypermedia-driven responses without breaking existing clients.
Consumer Ease
It is easy for consumers who rely on auto-generated documentation or tools that create client code directly from the API spec.
It is more complex for consumers since they need to interpret responses and follow hypermedia links to discover further actions.
API Evolution
Any change in the API structure requires updating the Swagger spec, regenerating client code, and distributing it to users.
HATEOAS allows easier changes as the client discovers the API through hypermedia, requiring fewer updates when the API evolves.
Versioning
Swagger typically requires explicit versioning and the maintenance of multiple versions of the API separately.
HATEOAS evolves without strict versioning because clients can dynamically follow the provided links.
HATEOAS focuses on dynamically guiding clients through API interactions using hypermedia links embedded in responses. At the same time, Swagger (or OpenAPI) provides static, human-readable, and machine-readable API documentation describing the API’s structure, endpoints, and operations.
5. Conclusion
In this article, we learned about Swagger and HATEOAS, with some application examples highlighting the main differences. We saw how to generate source code from a YAML template or use Swagger annotations to decorate our endpoints. For HATEOAS, we saw how to improve our model definition by adding valuable links to navigate all the resources related to an endpoint.
As always, the code is available over on GitHub.