Swagger 與 HATEOAS 的區別

REST
Remote
1
02:49 AM · Dec 01 ,2025

1. 概述

設計 REST API 的兩種常用方法是 Swagger 和 HATEOAS。兩者都旨在使 API 更易於使用和理解,但遵循不同的範式。

在本教程中,我們將瞭解 Swagger 和 HATEOAS 的區別以及一些常見用例。

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.

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.1. 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 = NewUser.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. Swagger UI example for a pet store.

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. Swagger 和 HATEOAS 之間的關鍵差異

讓我們説明一下 Swagger 和 HATEOAS 的區別:

方面 Swagger HATEOAS
API 文檔 Swagger 提供詳細、可讀的 API 文檔,帶有 UI,允許消費者提前瞭解可用的端點、請求參數和響應。 HATEOAS 依賴於服務器在響應中返回的超媒體鏈接,因此文檔更具隱性。消費者通過這些鏈接動態地發現操作,而不是預生成的 UI。
客户端實現 客户端通常基於 Swagger 規範生成或編寫。API 的結構在先,客户端可以根據預定義的路徑進行請求。 HATEOAS 客户端通過響應中的超媒體鏈接與 API 動態交互,無需事先了解 API 的完整結構。
靈活性 Swagger 更加僵化,期望預定義的端點和一致的 API 結構。這使得在不更新文檔或規範的情況下演化 API 變得更加困難。 HATEOAS 提供了更大的靈活性,允許 API 通過更改超媒體驅動的響應而演化,而無需破壞現有客户端。
消費者易用性 對於依賴於自動生成的文檔或直接從 API 規範創建客户端代碼的消費者來説,很容易。 對於消費者來説,由於他們需要解釋響應並遵循超媒體鏈接以發現進一步的操作,因此更加複雜。
API 演化 API 結構中的任何更改都需要更新 Swagger 規範、重新生成客户端代碼並將其分發給用户。 HATEOAS 允許更輕鬆地進行更改,因為客户端通過超媒體來發現 API,因此在 API 演化時需要更少的更新。
版本控制 Swagger 通常需要顯式版本控制和維護 API 的多個版本。 HATEOAS 在客户端通過鏈接動態地跟隨 API 時,版本控制變得不那麼嚴格。

HATEOAS 專注於使用響應中嵌入的超媒體鏈接來引導客户端通過 API 交互。同時,Swagger(或 OpenAPI)提供靜態、可讀的、可供機器使用的 API 文檔,描述 API 的結構、端點和操作。

5. 結論

在本文中,我們學習了 Swagger 和 HATEOAS,並通過一些應用程序示例強調了它們之間的主要區別。我們看到了如何從 YAML 模板生成源代碼,或者使用 Swagger 註解來裝飾我們的端點。對於 HATEOAS,我們看到了如何通過添加有價值的鏈接來改進模型定義,從而導航與端點相關的所有資源。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.