WS API REST - fidransky/kiv-pia-labs GitHub Wiki

TOTD:

  • learn about web service (WS) APIs in general
  • create API specs in OpenAPI
  • implement RESTful API using API-first approach

Terms and theory

WS API

Web service APIs in general are application APIs callable from other applications (also client) using web facilities. Various protocols may be used on the web: HTTP and SSE, WebSocket, RSocket, gRPC etc. Additionally, different data formats may be used within a protocol: JSON, XML, YAML - you name it. It's all WS APIs.

As with any API, what makes a good WS API is not only its implementation but (just as importantly) its documentation. Without an up-to-date, exhaustive and precise documentation, clients are forced to try what works and fail repeatedly, rather than implement an optimal solution on the first(-ish) try.

API-first vs. code-first approach

In API-first approach, solution architects design the API first and test it in the wild, without implementing a single line of code. Only then, programmers grab the API specs and start implementing the API - possibly the server- and client-side at the same time. Ideally, the API specs are used to generate some baseline code, making the code generation a part of the development process. That way, the API is guaranteed to remain in sync with the documentation.

In code-first approach, the API is implemented first and later, the API documentation is extracted from the code. Until the code is finished and annotated, API documentation is non-existent, meaning that parallel development of WS API server and clients is not possible. While suboptimal, this approach may still be used in cases where API is already implemented - you just sprinkle the code with annotations and generate the docs.

REST API

REST API evolved from the HTTP protocol and it led to some enhancements to the protocol itself, mainly introduction of new methods and status codes.

The important thing to remember about REST is that:

REST is an Architecture, Not a Standard

It means you can come accross many different (often wrong) ways of implementing it while still being called REST API. Nevertheless, there are a few guidelines and best practices that should make your APIs readable, understandable and maintainable.

Please, read this short summary on REST API to understand the basics, namely:

  • HTTP methods and their usage
  • HTTP status codes and their meaning
  • Resource naming aka URI assembly

Additionally, take a look at Richardson Maturity Model where steps toward REST API are described using four hierarchy levels.

diagram showing four levels of Richardson's REST maturity model

JSON, XML, YAML or what?

REST API does not force use of any specific data format but you're most likely going to see it paired with JSON.

The important thing to remember is that the actual format is also a part of the API contract - server and its clients must agree on at least one data format to be able to work together. This negotiation is usually performed using Accept and Content-Type HTTP headers.

Swagger & OpenAPI

Historically, there have been many tools whose goal it is to assist developers in the API creation, documentation, implementation and testing:

  • JSON Schema
  • API Blueprint (by Apiary)
  • RAML
  • Swagger

We are covering OpenAPI (evolved from Swagger) here. While not without a fair share of flaws, it proved to be the API specification wars winner. Nowadays, there's a multitude of tools and integrations based on OpenAPI. To pick a few:

  • OpenAPI Generator - Generator of client and server source code, documentation and more.
  • Swagger Editor - Online OpenAPI specs editor with syntax highlighting and linting. Not necessarily useful since OpenAPI specs are just a simple YAML file, easily editable in any text editor.
  • Redoc - Amazing API documentation generator.
  • Prism - Mock server and contract testing tool.
  • Postman - Desktop app for using WS APIs with a possibility to import OpenAPI specs. Alternatives: Bruno, Insomnia

Practice

1. Create OpenAPI specs

Use online Swagger Editor (or any text editor of your choice) to create OpenAPI specs of your app's REST API. To start, read description of Semester Project to decide which model objects, API endpoints etc. are needed. Check OpenAPI Specification to learn how to create OpenAPI specs.

Create a new src/main/resources/api.yaml OpenAPI specs file.

Document your existing endpoints in the OpenAPI specs file. In my case, I have implemented the following endpoints in HelloWorldController:

  • GET /hello?from={from} where from is a query parameter, returning HTML markup
  • GET /number returning random number as plain text

2. Configure OpenAPI generator Maven plugin

OpenAPI generator provides an extensively configurable Maven plugin which can be used to generate both server- and client-side source code baseline. It comes with many different generators, one of which is spring server generator.

Add OpenAPI annotations to the dependencies section of your pom.xml file:

<dependency>
	<groupId>io.swagger.core.v3</groupId>
	<artifactId>swagger-annotations-jakarta</artifactId>
	<version>2.2.23</version>
</dependency>

Add Jakarta Validation API annotations to the dependencies section of your pom.xml file:

<dependency>
	<groupId>jakarta.validation</groupId>
	<artifactId>jakarta.validation-api</artifactId>
	<!-- NOTE: we don't have to define version here, Spring Boot defines the compatible version for us
	<version>3.0.2</version>
	-->
</dependency>

Next, add OpenAPI generator plugin to the build > plugins section, copying the configuration from Github:

<plugin>
	<groupId>org.openapitools</groupId>
	<artifactId>openapi-generator-maven-plugin</artifactId>
	<version>7.8.0</version>
	<executions>
		<execution>
			<phase>generate-sources</phase>
			<goals>
				<goal>generate</goal>
			</goals>
			<configuration>
				<!-- copy the configuration from GitHub -->
			</configuration>
		</execution>
	</executions>
</plugin>

3. Generate the baseline code

With the OpenAPI generator Maven plugin configured, let's use it to generate some source code. The plugin is configured to execute in generate-sources phase of Maven lifecycle. Run mvn compile to execute it.

Then, open target/generated-sources/openapi/src/main/java/ folder to see what it generated.

Finally, mark the target/generated-sources/openapi/src/main/java/ folder as Generated Sources Root (in IntelliJ IDEA, right click on the folder and select Mark Directory as).

4. Implement the generated interfaces

The plugin is configured to only generate interfaces (note the interfaceOnly setting), leaving their implementation up to the developer (i.e. you).

Implement the following interfaces as Spring @RestControllers using core services (never directly repositories!) from cz.zcu.kiv.pia.labs.service package.

4.1 Implement DamagesApi interface

Add a new DamageController class implementing DamagesApi interface, annotated with @RestController in cz.zcu.kiv.pia.labs.controller package:

package cz.zcu.kiv.pia.labs.controller;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class DamageController implements DamagesApi {
}

Use IDE to bootstrap methods of the interface - for example, by pressing Alt+Shift+Enter.

As an example, let's implement the retrieveDamage endpoint now. Use dependency injection to autowire DamageService to the controller. Then, call DamageService#retrieveReportedDamage method to retrieve damage reports:

var result = damageService.retrieveReportedDamage();

return ResponseEntity.ok(result);

As expected, this doesn't work: the DamageService#retrieveReportedDamage method returns a collection of cz.zcu.kiv.pia.labs.domain.Damage while we need to return the generated model class. We need to map Damage to DamageDTO.

There's many ways how to map one object to another: libraries such as Dozer or MapStruct serve exactly this purpose. Here, to keep things simple, we're going to use Spring's built-in ConversionService.

To map custom objects, Spring's ConversionService uses converters. Implement one such converter mapping Damage to DamageDTO:

package cz.zcu.kiv.pia.labs.converter.rest;

import cz.zcu.kiv.pia.labs.domain.Damage;
import cz.zcu.kiv.pia.labs.model.DamageDTO;
import org.springframework.core.convert.converter.Converter;

public class DamageConverter implements Converter<Damage, DamageDTO> {
	@Override
	public DamageDTO convert(Damage source) {
		return new DamageDTO()
				.id(source.getId())
				.description(source.getDescription())
				// other mappings here
				;
	}
}

Next, configure ConversionService to use the newly created converter (implemented using Spring MVC, implement WebFluxConfigurer if you're using Spring WebFlux):

package cz.zcu.kiv.pia.labs.converter.converter;

import cz.zcu.kiv.pia.labs.converter.rest.DamageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.reactive.config.WebMvcConfigurer;

@Configuration
public class RestConverterConfiguration implements WebMvcConfigurer {
	@Override
	public void addFormatters(FormatterRegistry registry) {
		registry.addConverter(new DamageConverter());
	}
}

Finally, autowire ConversionService to the controller and use it to map the collection of Damages to DamageDTOs:

var result = damageService.retrieveReportedDamage().stream()
		.map(damage -> conversionService.convert(damage, DamageDTO.class))
		.toList();

return ResponseEntity.ok(result);

Sources

⚠️ **GitHub.com Fallback** ⚠️