The API is built in Java with Spring Boot and PostgreSQL. Gradle is used as the build tool.
The API endpoints are documented with the OpenAPI Specification 3.0.
To avoid dealing with a single large file, the
parameter, response and schema components are documented in seperated files.
During the deployment of the documentation to GitHub Pages, the files are bundled into a
Swagger UI compliant file
(see Bundle and deploy OpenAPI to GitHub Pages
workflow).
Thanks to a feature of the Redocly CLI,
which is used for linting and bundling of the documentation, a public and an internal documentation
can be generated, as paths for the public documentation can be excluded if the x-internal
field is set to true
.
In order to lint the OpenAPI documentation locally, as done by the
Lint OpenAPI
workflow, install the Redocly CLI globally via npm:
> npm install @redocly/cli@"^1.0.2" -g
and run:
> redocly lint api-internal --config ./api/openapi/redocly-cli-config.yml
The following model represents the domain layer:
VO stands for Value Object, which leads to an implementation as Embeddable in Spring.
There cannot be several races with the same name in one season.
A team of a season must have at least 2 drivers.
There cannot be multiple instances of TeamOfSeason for the same team of a season.
As a driver can be in multiple teams of a season, DriverOfRace instances also save the team for that a driver has driven in a race.
There cannot be multiple instances of DriverOfRace for the same driver of a race.
Every entity from the domain model has its own package, which is dived into a domain and an application package. The first contains the entity and repository, while the latter is used for the application logic. The Controller, Application Service and DTO can be found here.
To encapsulate domain objects, only DTOs are passed to the outside.
The mapping between a domain object and the corresponding DTO is implemented with the
MapStruct mapping framework in a *Mapper
class of the application package
(see e.g. SeasonMapper
).
DTOs passed with a request are validated with
Spring's Bean Validation.
As the validation constraints are not always the same for create and update operations, the validation groups
OnCreate
and
OnUpdate
are used to
specify when a constraint should apply, such as in the
RaceDTO
.
More information about validation with Spring can be found in this blog post.
ApiException
acts as abstract base exception that is extended by all project specific
exceptions.
Every exception that is thrown is caught by the
ApiExceptionHandler
, which creates and returns a corresponding
ApiExceptionInfo
from the caught exception.
Paging is used to reduce network traffic if only a chunk of data is needed. General information about paging with Spring can be found in these blog posts on Reflectoring and HowToDoInJava.
The sort
query parameter can be used to sort entities by their properties.
As a PropertyReferenceException
is thrown, if a given sort property does not exist in the entity, properties that are named differently in the entity
and the corresponding DTO must be mapped to each other.
This mapping is implemented in the
SortPropertyMapper
interface.
It defines a getSortProperties
and a map
function.
The first returns a map of the related sort properties,
where the keys are the property names in the DTO and the values are the corresponding property names in the entity.
The concrete map is defined in the SortPropertyMapper
implementation for the corresponding entity
(see e.g. SeasonSortPropertyMapper
).
The latter uses the map to create a valid Sort
object: For each sort property of the given Sort
object is checked
whether it is a key in the sortProperties
map.
If this is the case, the sort property is replaced by the corresponding property name of the entity.
If not, it is assumed that the property has the same name in the entity and the DTO, and it is checked whether
the sort property is contained in the properties
list
(cf. e.g SeasonDTO
) of the DTO and whether it is not an ID,
as there is no point in sorting objects by their ID.
If true, the property is left as it is. Otherwise, an
ApiInvalidSortPropertyException
is thrown.
Since the Sort
object is encapsulated inside a Pageable
object, the map
function is called
from the handleSortProperties
functions of the
SortPropertyHandler
, which create a new
Pageable
instance that can be passed to repository methods.
Spring ignores invalid query parameters by default. For example, the GET Request
/seasons?foo=bar
would be interpreted as GET /seasons
. As this behaviour is not intuitive, the
checkIfParametersAreValid
method of the
QueryParameter
interface checks if given query parameters
are contained in the queryParameters
set, defined for the corresponding entity and, if not,
throws an ApiInvalidQueryParameterException
.
To check given query parameters and sort properties together the handleQueryParameters
functions of the
QueryParameterHandler
are used.
Instead of passing the complete page object as response body, only
the properties totalElements
, totalPages
, pageNumber
, pageSize
and content
of the page are returned as an instance of
ResponsePage
.
Specifies the Spring Profile. Available options are prod
and dev
.
Depending on which profile is used, either the configurations defined in
application-prod.yml
or
application-dev.yml
take effect.
If set to dev
, starting the application will also start the postgres service, specified
in the compose-dev.yml
, via the
Spring Docker Compose Support.
Specifies the default pageSize
used for paging of collections.
Default value: 25
Specifies the maximum allowed pageSize
.
Default value: 100
Specifies the PostgreSQL URL.
Default value: //postgres:5432/postgres
Specifies the PostgreSQL user.
Specifies the password for the specified DB_USERNAME
.
The Dockerfile
, used to build the image of the API available on
DockerHub, is based on suggestions of
this Spring Guide. It makes use of
Multi-stage builds and JAR layers to
speed up subsequent builds.
The Eclipse Temurin JRE image is used as base image.
To build a container image from the Dockerfile, run:
> docker build -t <imageName>:<imageTag> --pull [--no-cache] .
Multi-platform images can be built with builder instances whose driver type is docker-container
.
This command creates a builder with the name multi-platform-builder
:
> docker buildx create --driver docker-container --name multi-platform-builder
To use the created builder to build an amd64
and arm64
linux image, execute:
> docker buildx --builder multi-platform-builder build \
--push \
-t <imageName>:<imageTag> \
--platform linux/amd64,linux/arm64 \
--pull \
[--no-cache] .
Notice: The --push
option will try to push the built images to DockerHub. To push them to a local registry, take
a look at this
blog post. Unfortunately, this seems to be the only easy way to extract the built images, as Docker currently does
not support loading of multi-platform images in a stable way and therefore the --load
option does not work
(cf. GitHub Issue).
Notice: Set SPRING_PROFILES_ACTIVE
to prod
, as dev
is meant to be used for local development only
and does not work when running the application via Docker with the provided image.
This project uses the palantir-java-format. To format files accordingly, execute:
> ./gradlew spotlessJavaApply
To check whether all files comply with the format, as done by the
Spotless check
workflow, run:
> ./gradlew spotlessJavaCheck