Skip to content

Workshop #3: Enhancing API layer

Marcin Zielonka edited this page Nov 27, 2022 · 24 revisions

About 📝

Workshop #3 is about enhancing the API layer - not only the REST one but the API contract in general (e.g. between modules communicating with each other). Therefore, you can find topics such as:

  • HTTP basics (methods, status codes, etc.)
  • REST API conventions
  • Swagger definitions
  • Java components visibility
  • SOLID

This workshop

Source code 🗃

You can find the source code that represents the status of the project after this workshop - TBD


HTTP basics

Let's recollect what the HTTP is and what is the proper way to use it in terms of building efficient and readable REST APIs :)

What is HTTP

HTTP stands for Hyper Text Transfer Protocol. This protocol is used to transfer data (HTML documents, CSS/JS files, JSON data, etc.) over the Internet between clients and servers via HTTP requests and HTTP responses.

HTTP methods

We can distinguish 5 mostly used HTTP methods: GET, POST, PUT, DELETE, PATCH (full list available on https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods site).

HTTP Method Description Safe Idempotent Has request body Has response body
GET Read a resource Yes Yes Optional Yes
POST Create a new resource No No Yes Yes
PUT Update a resource No Yes Yes Yes
DELETE Delete a resource No Yes Optional Optional
PATCH Update partially a resource No No Yes Yes

HTTP status codes

HTTP response status codes indicate the result of the processed HTTP request. They are grouped into five classes:

  • 1xx: Informational responses
  • 2xx: Successful responses
  • 3xx: Redirection responses
  • 4xx: Client error responses
  • 5xx: Server error responses

A full list of HTTP status codes can be found at https://developer.mozilla.org/en-US/docs/Web/HTTP/Status site.

HTTP request structure

Let's take a sample GET request:

https://api.example.com:3000/shop/v1/items?category=fruits

and decompose it into the parts:

  • http:// - request protocol (http or https)
  • api.example.com - host
  • 3000 - port (for http default port is 80 and for https is 443)
  • /shop/v1/items - request URI
  • ?category=fruits - query params

Additionally, an HTTP request can contain:

  • request body (it can be in plain text, x-www-form-urlencoded, binary, JSON, etc.)
  • request headers

HTTP response structure

Each HTTP response contains:

  • response headers
  • response body
  • HTTP status code

REST API

What is REST API

According to Red Hat, REST API (also known as RESTful API) is an application programming interface (API or web API) that conforms to the constraints of REST architectural style and allows for interaction with RESTful web services. REST stands for representational state transfer and was created by computer scientist Roy Fielding.

An API is a set of definitions and protocols for building and integrating application software. It’s sometimes referred to as a contract between an information provider and its consumer.

How to build a good REST API

Let's see what rules we should follow to build an understandable and efficient REST API.

Use HTTP methods to describe an action

You should NOT use any verbs in the request URI in a designed REST API. This should be done by using the proper HTTP method (see HTTP methods table for more details).

Bad practice:

GET http://api.example.com/shop/v1/list-items
POST http://api.example.com/shop/v1/items/create

Good practice:

GET http://api.example.com/shop/v1/items
POST http://api.example.com/shop/v1/items

Request URI to describe a resource

The request URI should always describe the resource which you will manage (the HTTP method indicates in which way). It is also important to design proper resource nesting.

Bad practice:

GET http://api.example.com/shop/v1/items/by-id/1
GET http://api.example.com/shop/v1/items/employees

Good practice:

GET http://api.example.com/shop/v1/items/1
GET http://api.example.com/shop/v1/basket/items

Use kebab-case for endpoint naming

You should use kebab-case naming convention while building request URIs to keep good readability.

Bad practice:

GET http://api.example.com/shop/v1/shopEmployees
GET http://api.example.com/shop/v1/shop_employees

Good practice:

GET http://api.example.com/shop/v1/shop-employees

Return proper status codes

You should always return proper status code that informs about the actual status (e.g. if request has been processed successfully or client provided invalid data in request body). You can find a full list of status codes with detailed descriptions at https://developer.mozilla.org/en-US/docs/Web/HTTP/Status site.

Query params for filtering

For reading data requests (the GET ones) you can provide filtering support using query parameters instead of e.g. creating a new endpoint.

Bad practice:

GET http://api.example.com/shop/v1/fruit-items
GET http://api.example.com/shop/v1/items/fruit-and-vegetables
GET http://api.example.com/shop/v1/items?type=fruit
GET http://api.example.com/shop/v1/items?type=fruit&type=vegetable

Support pagination

You can also consider providing support for pagination - especially if you expect large amount of items returned in a responses for reading specific data. Pagination parameters consists of two properties:

  • limit: how many entries should be returned in response
  • offset: number of entries that should be skipped before selecting records

Example:

GET http://api.example.com/shop/v1/items?limit=100&offset=300

It tells us to retrieve maximum of 100 items but skipping the first 300 ones

Versioning

You should also consider providing support for API versioning. For example, you want to offer a breaking change in your system that will change system behavior but you have to keep the contract established with your current API. Therefore, you can create version two of the API that will have a new contract with the consumer while the old one stays untouched.

Example:

GET http://api.example.com/shop/v1/items
GET http://api/example.com/shop/v2/items

Standardized error responses

You should also provide a unified and consistent response structure for any errors returned by your API (e.g. the client errors or the server ones) because lots of consumers of your API can be other systems that not understand like human. Therefore, standardized error responses can be very helpful for consumers to easily provide error handling on their side.

Example:

{
    "type": "http://api.example.com/errors/v1/item-not-found",
    "title": "Item Not Found",
    "detail": "Item with id '12' not found",
    "status": 404
    "instance": "/items/12"
}

More details about designing problem responses can be found in RFC document at https://www.rfc-editor.org/rfc/rfc7807.


Swagger

What is Swagger

Swagger is a suite of tools for API developers from SmartBear Software and a former specification upon which the OpenAPI Specification is based

What is OpenAPI

According to the official site, OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.

An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases


Building efficient REST API layer with Spring Boot

Now let's dig into code and see how we can write readable and easily extensible REST APIs in Spring Boot.

Use DTOs for both request body and responses

This part will available after workshops 📽

Delegate all business logic to the service layer

This part will available after workshops 📽

Handle errors

This part will available after workshops 📽

Apply Swagger

This part will available after workshops 📽


Building scalable domain-driven application

Apart from the REST API layer, we should also take care of rest of the project components to make the whole system scalable and maintainable. Therefore, one of the ways is to properly group code/components by their functionality into so-called domains.

Group code by domain

This part will available after workshops 📽

Apply proper component visibility

This part will available after workshops 📽

One responsibility per component

We should always try to create components that are responsible for doing one thing - it makes the application structure more readable and a lot easier to test. As a result, we have reusable parts that we can use to create application flows (e.g. create an item, read list of items in a basket, etc.).

Here is an example of a BasketController. This is a controller, so it is a part of a presentation layer. Therefore, it should be responsible only for handling requests from a user and providing a response in a proper format. It should not contain any code that is strictly related to business logic (e.g. creating a new entry in a database).

@RestController
@RequestMapping("/baskets")
public class BasketController {

    private final BasketStorage basketStorage;
    private final BasketMapper basketMapper;
    private final BasketItemMappper basketItemMappper;

    public BasketController(BasketStorage basketStorage, BasketMapper basketMapper, BasketItemMappper basketItemMappper) {
        this.basketStorage = basketStorage;
        this.basketMapper = basketMapper;
        this.basketItemMappper = basketItemMappper;
    }

    @Operation(description = "Create a new basket")
    @ApiResponses({
            @ApiResponse(description = "Successfully created basket",
                    responseCode = "201",
                    content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = BasketResponse.class))
            ),
            @ApiResponse(description = "Failed to create basket",
                    responseCode = "400",
                    content = @Content(mediaType = MediaType.APPLICATION_PROBLEM_JSON_VALUE, schema = @Schema(implementation = ErrorResponse.class))
            ),
            @ApiResponse(description = "Unauthorized attempt to create basket",
                    responseCode = "401",
                    content = @Content(mediaType = MediaType.APPLICATION_PROBLEM_JSON_VALUE, schema = @Schema(implementation = ErrorResponse.class))
            ),
    })
    @PostMapping
    public ResponseEntity<BasketResponse> create(@RequestBody BasketCreateRequest createRequest) {
        var createdBasket = basketStorage.createBasket(createRequest);
        return new ResponseEntity<>(basketMapper.toBasketResponse(createdBasket), HttpStatus.CREATED);
    }

     // other endpoints implementation in an analogical way

}

We can see that this implementation of the controller is responsible for:

  • providing API documentation via Swagger annotation
  • exposing an endpoint for creating a new basket
  • delegating handling creating a new basket to a BasketStorage
  • mapping response returned from BasketStorage to a proper format defined in API docs via BasketMapper
  • returning the response with proper HTTP status code

These all aspects are related to a responsibilities of presentation layer.

This part will available after workshops 📽

Clone this wiki locally