YADVS (Yet Another Digital Voting System) – is a showcase project demonstrating the creation and management of polls and votes within small communities.This specification serves as a comprehensive guide for building a RESTful API for YADVS, adhering to industry best practices.
This document outlines standard HTTP methods for managing voting resources, efficient data retrieval features, error handling through structured responses, and clear versioning with HATEOAS support to ensure robust, future-proof APIs.
URI Considerations
Choosing the API Root
/yadvs/apivs./yadvs/rest:- consideration:
/yadvs/api– provides a more flexible and future-proof approach for API endpoint design./yadvs/rest– explicitly communicates adherence to RESTful standards.
- conclusion: since the purpose of the project is exercising REST best practices the choice will be
/yadvs/rest.
- consideration:
/yadvs/rest/v1vs/yadvs/v1/rest:- consideration:
/yadvs/rest/v1– adheres to common API design conventions./yadvs/v1/rest– can be useful when planning to have multiple versions coexisting for a long time.
- conclusion:
/yadvs/rest/v1– because of adherence to best practices.
- consideration:
API Versioning Considerations
The envisioned API versioning strategy will incorporate two versioning schemes to effectively manage changes in the digital voting system API, both for breaking changes and backward-compatible revisions.:
- URI Path Verioning for braking changes:
/yadvs/rest/v1for the PoC version of the digital voting system/yadvs/rest/v2for the API that will registered users, roles, AuthN, and AuthZ.
- Query String Versioning for backward compatible versions (let’s call them revisions):
/yadvs/rest/v2?~revision=.0.0or simply/yadvs/rest/v2for the first stable release of version 2.- Subsequent releases will increment revision values in accordance with semantic versioning, enabling clients to adopt updates as necessary.
This dual-versioning strategy allows for flexibility by accommodating both major (breaking) and minor (non-breaking) changes, helping maintain API stability and ease of use. It also ensures a clear separation between significant updates that may impact client compatibility and revisions that maintain backward compatibility, supporting a smooth and manageable client integration experience.
Resource Endpoints Group
The REST request-response format is largely consistent with best practices, with the following refinements.:
- POST
/yadvs/rest/v1/polls- creates a new Poll resource.
- GET
/yadvs/rest/v1/polls/{id}- returns a representation of the Poll resource identifed by {id}.
- PUT
/yadvs/rest/v1/polls/{id}- replaces all the Poll resource attributes with the values specified in the request body.
- PATCH
/yadvs/rest/v1/polls/{id}- replaces only the Poll resource attributes that were specified in the request body that are not null.
- only
application/merge-patch+jsonis supported for now.
- DELETE
/yadvs/rest/v1/polls/{id}- removes the Poll resource identified by {id}.
- GET
/yadvs/rest/v1/polls- allows the retrieval of all the Poll resources or only a subset based on the query parameters;
- the call returns the total number of records in the
X-Total-Countheader. This is especially useful when using pagination; - the call also returns the total number of pages in the
X-Total-Pagesheader when~pageSizeis provided.
All responses include the X-Correlation-ID header to help track requests across services or logs (in anticipation for distributed systems).
The API’s detailed specification and documentation are available through Open API and Swagger UI, utilizing the org.springdoc:springdoc-openapi-starter-webmvc-ui module.
Query Parameters
All field names must be valid Java identifiers or variable names.
Projection
Projection represents the list of fields / columns that are to be returned by the service. It is specified by the ~fields parameter and its value is a comma separated list of field names e.g.:
~fields=name– will only return the name values~fields=id,name,description– will only return the id, the name, and the description of the resources~fields=– will return all the available fields
Example: /yadvs/rest/v1/polls?~fields=name,description – will only populate name and description fields in the returned list of records.
Selection
Selection represents a set of criteria used to identify the set of records that are to be returned by the service. It is a set of logical constraints used in conjunction. To make it compatible with the syntax of HTTP request each clause has 3 parts:
- the name – this mandatorily starts with the field name and optionally can have a logical operator attached using ~. The supported operators are:
field~eq– test for equality. It can be omitted sinceage~eq=35has the same semantics asage=35.field~ne– test for non-equality.field~is– equality test for boolean values (true, false) or special values (null, notnull)field~lt– test for less thanfield~le– test for less than or equal tofield~gt– test for greater thanfield~ge– test for greater than or equal tofield~like– test for string matchingfield~unlike– test for string mismatchingfield~in– test for value in list (e.g. status~in=ACTIVE,DRAFT)
- the
=sign – as used by HTTP specification - the value – as a string representation
Except for the ~is operator, the type of the field (string, numeric, temporal, etc.) is identified on the server side based on resurce specification.
Example: /yadvs/rest/v1/polls?description~like=lunch&status=ACTIVE&end~gt=2024-10-20T14:00:00 – will return all the active polls related to lunch that will end after 2 o’clock in the afternoon on a certain date.
Sorting
Sorting refers to the order in which the records are returned. It is defined by a comma separated array of fields each field indicating either ascending or descending order. The direction of the order is indicated by the field prefix that can be either + (ascending) or – (descending). The prefix can be omitted in which case the direction will be ascending. The name of the query parameter specifoying the ordering is ~sort.
Example: /yadvs/rest/v1/polls?~sort=-status,name – will return the records first ordered descendingly by the status code and then ascendingly based on the poll name.
Pagination
Pagination is a technique used to efficiently handle large datasets in web APIs. Instead of returning all data at once, which can be inefficient for large sets, pagination allows for data to be returned in smaller, more manageable chunks. This is implemented by using two query parameters:
~pageNo– indicates the page number. Rmarks:- the indexes start from 1;
- the minimum allowed value is 1;
- if specified then
~pageSizemust also be specified.
~pageSize– specifies the maximum number of records that will be returned in one page. Remarks:- the minimum allowed value is 1;
- if
~pageNois not specified than~pageNois automatically set to 1.
Example: /yadvs/rest/v1/polls?~pageNo=1&~pageSize=10 – will return at most the first 10 polls.
Response Structure
The REST responses tend to follow the JSON Hypertext Application Language specification HAL-2024.
HATEOAS
HATEOAS is supported by including the dedicated Spring Boot dependency (org.springframework.boot:spring-boot-starter-hateoas) and making use of the provided API classes like org.springframework.hateoas.RepresentationModel or org.springframework.hateoas.CollectionModel.
In order to receive the HATEOAS links (in the body of the response) the client must add the HATEOAS value in the Accept-Links header.
Single Resource Response
GET http://localhost:8080/yadvs/rest/v1/polls/1
{
"id": 1,
"name": "Customer Markets Specialist",
"description": "Et iste accusamus sint qui ...",
"status": "DRAFT",
"multiOption": false,
"start": null,
"end": null,
"_links": {
"self": {
"href": "http://localhost:8080/yadvs/rest/v1/polls/1"
}
}
}
Multiple Resources Response
GET http://localhost:8080/yadvs/rest/v1/polls?~fields=id,name,status,multiOption&~sort=-name&name~like=d&~pageNo=1&~pageSize=2
X-Total-Count: 4
{
"_embedded": {
"pollList": [
{
"id": 7,
"name": "Legacy Branding Liaison",
"description": null,
"status": "DRAFT",
"multiOption": true,
"start": null,
"end": null,
"_links": {
"self": {
"href": "http://localhost:8080/yadvs/rest/v1/polls/7"
}
}
},
{
"id": 18,
"name": "Lead Directives Orchestrator",
"description": null,
"status": "DRAFT",
"multiOption": false,
"start": null,
"end": null,
"_links": {
"self": {
"href": "http://localhost:8080/yadvs/rest/v1/polls/18"
}
}
}
]
}
}
Error Responses
There is a unified approach to error reporting. By using @RestControllerAdvice and @Check aspect (instead of @Valid) it is ensured that all Spring Boot internal errors return consistent JSON structures.
Here is an example of a validation issue when trying to create a poll with empty name and description:
{
"status": 400,
"path": "/yadvs/rest/v1/polls",
"timestamp": "2024-10-20T15:49:00.243+03:00",
"logref": "6NnynxD8tHcarke6WU0Dv",
"_embedded": {
"errors": [
{
"code": "2001: not_empty",
"message": "must not be empty",
"target": "description",
"targetType": "FIELD",
"_links": {
"swagger": {
"href": "http://localhost:8080/swagger-ui.html"
}
}
},
{
"code": "2001: not_empty",
"message": "must not be empty",
"target": "name",
"targetType": "FIELD",
"_links": {
"swagger": {
"href": "http://localhost:8080/swagger-ui.html"
}
}
}
]
}
}
status– the HTTP response codepath– the request URItimestamp– a timestamp (server time) registered at the request time – used for efficient log searchlogRef– a friendly UUID generated at request time – it is used to mark all relevant log entrieserrors– a list of all the issues identified with the request and each of them contains:- code – a classifying code error together with a short description
message– a detailed message describing the issuetarget– an identifier of the issue sourcetargetType– HEADER, PARAMETER, FIELD, or URI_links– a set of links that could be useful for understanding and fixing the issue
The response message provides optional support for RFC 9457 (Problem Details for HTTP APIs) for fields type, title, detail, instance in addition to status.
Example response for an issue related to using a wrong selection operator (http://localhost:8080/yadvs/rest/v1/polls?name~x=a):
{
"status": 400,
"path": "/yadvs/rest/v1/polls",
"timestamp": "2024-10-20T16:07:06.945+03:00",
"logref": "4yIxjsXqjwPjM8HrEZdTrx",
"_embedded": {
"errors": [
{
"code": "3220: selection_criteria",
"message": "Cannot parse selection predicate operator `x'.",
"target": "name~x",
"targetType": "PARAMETER",
"_links": {
"swagger": {
"href": "http://localhost:8080/swagger-ui.html"
}
}
}
]
}
}
Even the Spring Boot’s internal error messages were intercepted and standardised (DELETE http://localhost:8080/yadvs/rest/v1/polls – no {id} specified):
{
"status": 405,
"path": "/yadvs/rest/v1/polls",
"timestamp": "2024-10-20T16:03:52.176+03:00",
"logref": "4ZfoholF0wL7e8MS3VYFwf",
"_embedded": {
"errors": [
{
"code": "1010: api_error",
"message": "Request method 'DELETE' is not supported",
"target": "/yadvs/rest/v1/polls",
"targetType": "URI",
"_links": {
"swagger": {
"href": "http://localhost:8080/swagger-ui.html"
}
}
}
]
}
}
Error Codes
In addition to the standard HTTP staus codes the error REST responses include in their bodies a set of application specific codes (the list will be extended while adding more features to the application):
- System Errors
- 1000: generic
- this is the default error code to be returned whan no other more specific can be identified.
- 1010: api_error
- this is used when the HTTP request cannot be properly mapped to a handler method (e.g. when a request handler does not support a specific request method).
- 1000: generic
- Validation Errors
- 2000: not_null
- used for a validation error raised by the @NotNull annotation and similar situations.
- 2001: not_empty
- used for a validation error raised by the @NotEmpty annotation and similar situations.
- 2100: not_allowed
- this is used when a user operation cannot be performed because it would violate a core business rule (e.g. deleting and active poll).
- 2101: type_conversion
- used in case if an illegal type conversion (e.g. a boolean value to a LocalDateTime).
- 2102: resource_conflict
- used to signal an attempt to create a resource that already exists and cannot be duplicated (e.g. creating two polls with the same name).
- 2000: not_null
- Query Parsing Errors
- 3210: projection_criteria
- returned when there is an error parsing the projection criteria (e.g. an invalid field name).
- 3220: selection_criteria
- returned when there is an error parsing the selection criteria (e.g. an invalid field name or an invalid operator name).
- 3230: sorting_criteria
- returned when there is an error parsing the sorting criteria (e.g. an invalid field name).
- 3240: pagination_criteria
- returned when there is an error parsing the pagination criteria (e.g. an invalid page or page size number).
- 3210: projection_criteria
There is no mandatory mapping between the the application error codes and the HTTP satus codes but generally 1000 maps to HTTP 500 (Internal Server Error), 1010 corresponds to to HTTP 405 (Method Not Allowed), and the rest of them to HTTP 400 (Bad Request).
Future improvements
- adding HATEOAS-based pagination links;
- add subresources (like options) and more HATEOAS links;
- address security concerns: Authentication, Rate Limiting, CORS;
- add more details to OpenAPI/Swagger documentation;
- WebSocket and GraphQL considerations.
Leave a Reply