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
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.
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 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.
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.
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
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}
wherefrom
is a query parameter, returning HTML markup -
GET /number
returning random number as plain text
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>
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).
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 @RestController
s using core services (never directly repositories!) from cz.zcu.kiv.pia.labs.service
package.
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 Damage
s to DamageDTO
s:
var result = damageService.retrieveReportedDamage().stream()
.map(damage -> conversionService.convert(damage, DamageDTO.class))
.toList();
return ResponseEntity.ok(result);