Expose a discoverable REST API of your domain model using HAL JSON as media type
•
Cristina Negrean •
6614 words
36 minute read •
Background
REpresentational State Transfer (REST) is an architectural style inspired by the Web and nowadays Hypermedia-driven CRUD REST services are at the backbone of a microservice internal architecture.
Benefits as scalability, ease of testing and deployment, as well as the chance to eliminate long-term commitment to a single technology stack, have convinced some big enterprise players
– like Amazon, Netflix,
eBay,
Spotify
– to begin transitioning their monolithic applications to microservices.
Purpose
If we want to accelerate microservices adoption rather than code at monolithic applications, it’s essential that we can create a basic service with a minimum of effort.
Writing a REST-based microservice that accesses a data model typically involves writing the Data Access Objects (DAO) to access the data repository and writing the REST methods independently.
This often means that you are responsible for writing all of the SQL queries or JPQL(Java Persistence Query Language).
The Spring Data REST framework provides you with the ability to create data repository implementations automatically at runtime,
from a data repository interface, and expose these data repositories over HTTP as a REST service.
It takes the features of Spring HATEOAS and Spring Data JPA and combines them automatically.
This guide will give you a high-level walk through of my process of creating a microservice - called Wanderlust - that exposes a discoverable REST API of a simple open-travel domain model using JSON HAL as media type.
Minimum Viable Product
list, search by country or name travel destinations
create, validate, update and delete a travel destination
list, search travel agents by name
create, validate, update and delete a travel agent
list, search holiday packages by destination country
create, validate a holiday package for a travel destination
pagination and sorting for list and search operations
cURL 7.0+ (cURL is installed by default on most UNIX and Linux distributions. To install cURL on a Windows 64-bit machine,
click here) or Postman
Your favorite Java code editor. At the moment my favorite one is IntelliJ IDEA
When running on OS X, installion steps can be found in the project’s README.
“Bootiful Wanderlust” with Spring Boot and Spring Initializr
Sometimes the hardest part of any project is getting started. You have to setup a directory structure for various project artifacts, create a build file and populate it with all library dependencies. This is where Spring Initializr comes into handy. Spring Initializr is ultimately a web application that can generate a Spring Boot project structure for you. It doesn’t generate any application code, but it will give you a basic project structure and either a Maven or a Gradle build specification to build your code with. All you need to do is write the application code.
Spring Boot, on the other hand, helps package the autonomous data microservice application as an uber-self-runnable-JAR (Java ARchive) with an embedded application container (Apache Tomcat - autoconfigured by default, Jetty, JBoss Undertow). Spring Boot also became “de-facto” way to create Spring applications.
Why Spring Boot?
It’s suitable for Cloud Native Applications that follow the 12factor patterns (developed by the Netflix engineering team)
Productivity increases by reducing time of development and deployment
Enterprise-production-ready Spring applications
Non-functional requirements, such as Spring Boot Actuator (a module that brings metrics, health checks and management features)
The most straightforward way to use the Spring Initializr is to point your web browser to http://start.spring.io. Fill in basic project information on the left side of the form, search and add dependencies listed in the figure below on the right side of the form and you should be all set to download the project template.
The Generate Project button will download the template project.
“REST-Assured”: Building Continuous Delivery confidence with Test Driven Development
Before even starting to write a line of code, it is a good practice to think about the approach to take to automate code quality checks so that you can code and refactor with confidence. And at the same time strive to minimize the number of defects!
While I will show you how to fully unit and integration test the back-end API in a later post, I’ll start by adding the tooling that calculates the percentage of code accessed by tests to the project build specification file (build.gradle).
The goal is to have each time when building the project, JaCoCo (Java Code Coverage) produce an XML report in the build directory: wanderlust-open-travel-api/build/reports/jacoco/test/jacocoTestReport.xml to be used with the Continuous Integration (CI) server of your choice - for me that is Travis - to visually quantify test coverage and its evolution between chronological software increments.
Go ahead and open the project template structure generated with Spring Initializr in your favorite IDE and add the jacoco and coveralls dependencies to the Gradle Build specification. The result should look similar to the listing below.
The Gradle IDEA plugin - apply plugin: 'idea' - generates files for working with the project in IntelliJ IDEA. To generate the project files, simply open a terminal window, navigate to the project root directory and run the idea task.
Cristinas-iMac:wanderlust-open-travel-api cristina$ ./gradlew idea
:ideaModule
:ideaProject
:ideaWorkspace
:idea
Creating and Initializing PostgreSQL Relational Data Store with Flyway and SQL
One of the design principles of microservices architecture is to have a separate data store for each microservice. If you got so far, you should already have a PostgreSQL data store instance running on your machine with a created database named wanderlust, as well as USERNAME_POSTGRES and PWD_POSTGRES environment variables configured and resolvable according to the project’s README
To check whether your connection to the database is working properly, you can open a command line tool and type in:
psql -h localhost wanderlust
You should see an output similar with to below:
psql (9.5.3)
Type "help" for help.
wanderlust=# help
You are using psql, the command-line interface to PostgreSQL.
Once that is working, we need to instruct Spring Boot to use PostgreSQL for storing the microservice data. As you might already know, Spring Boot is using by default H2 in-memory database. In-memory databases are useful in the early development stages in local environments, but they have lots of restrictions. As the development progresses, you would most probably require a persistent data store to develop and test your application before deploying it to use a production database server. Spring Boot integrates with all major RDBMS and NoSQL databases as: MySQL, PostgreSQL, Oracle, MongoDB, Cassandra, even Microsoft SQL Server. Isn’t Microsoft taking major steps towards open source? :)
To use PostgreSQL, you will need the proper database drivers. The required JARs should already be provisioned due to the fact that we have included it as a dependency with Spring Initializr. Furthermore we would like to use PostgreSQL with a lightweight, high-performance JDBC connection pool technology like HikariCP to prepare the service for production-grade environment concurrency.
This last one can be added to build.gradle file:
Additionally we will check that Spring Boot’s AutoConfiguration feature has yielded the proper connection properties and tweak them accordingly.
INSERTINTOdestinationsVALUES((SELECTnextval('destinations_id_seq')),'Quintessential Japan','Japan','Life-changing-experience');INSERTINTOdestination_factsVALUES((SELECTcurrval('destinations_id_seq')),'There are over 5.5 million vending machines in Japan selling everything from umbrellas and cigarettes to canned bread and hot noodles.'),((SELECTcurrval('destinations_id_seq')),'Japan''s birth rate has plummeted so significantly that adult nappies (diapers) outsell babies'' nappies, which are also sold in vending machines.'),((SELECTcurrval('destinations_id_seq')),'It is estimated that more paper is used for manga comics than for toilet paper in Japan. (Surprise: both are sold in vending machines as well.)'),((SELECTcurrval('destinations_id_seq')),'One of the world''s most famous pilgrimage routes after the Camino de Santiago is Japan''s Kumano Kodo near Osaka.');INSERTINTOtravel_agents(id,name,country,postal_code,email,website)VALUES((SELECTnextval('travel_agents_id_seq')),'Shoestring','NL','1114 AA','info@shoestring.nl','https://shoestring.nl');INSERTINTOholiday_packages(id,destination,depart_from,package_info)VALUES((SELECTnextval('holiday_packages_id_seq')),1,'Amsterdam Schipol','New horizons in the land of the rising sun');INSERTINTOtravel_agent_holiday_packagesVALUES((SELECTcurrval('travel_agents_id_seq')),(SELECTcurrval('holiday_packages_id_seq')));
Database scripts will be run automatically by Flywaydb upon service start. Before starting the service, I will change the default server port. Spring Boot default is 8080 and it supplies you with the server.port property for configuring the port on which our Spring Boot application should run.
You may be wondering: Wait a minute, what is this bootRun command? How can I install it? If you revisit Listing 1: build.gradle, line
apply plugin: 'org.springframework.boot' provides Spring Boot support in Gradle. The Spring Boot Gradle Plugin allows you to package executable jar or war archives. The default option for packaging in Spring Initializr is jar (Java ARchive) and that is also reflected in Listing 1: build.gradle
Listing 6 shows you the entry point for a Spring Boot application in Java. The main() method uses Spring Boot’s SpringApplication.run() method to launch an application. Did you notice that there wasn’t a single line of XML? No web.xml file either. This web application is 100% pure Java and you didn’t have to deal with configuring any plumbing or infrastructure. The run() method call accepts two parameters - the class annotated with @SpringBootApplication and the application’s arguments.
You can check the schema version and installation details via Spring Actuator endpoint or command line:
Flywaydb is a great tool that makes database migrations automation easy. It deserves in itself a separate blog post. I particularly like this one!
Coding the Domain Model
The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a travel destination, a holiday package, a tour operator/agent.
“In other words, any concept that might be the target of an author’s hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time.” - Roy Fielding’s dissertation
In this blog post I will exemplify Destination class which is a simple model of a travel destination resource, with four basic attributes: the name, country, description and off course some funny facts about the destination. Geodata (GIS) and photos anyone? ;) It is annotated with @Entity, indicating that it is a JPA entity, mapping to persistent storage: wanderlust.destinations and wanderlust.destination_facts schema tables.
The Destination class encapsulates validation constraints with the use of Hibernate Validator@NotEmpty and
JSR 303/349 Bean Validation@Size,@Pattern annotations. As you can see the name and the country of the destination are mandatory fields.
I have used the message attribute of the bean validation annotations to provide API clients with descriptive validation error messages.
packagecristina.tech.blog.travel.domain;importorg.hibernate.validator.constraints.NotEmpty;importjavax.persistence.CollectionTable;importjavax.persistence.Column;importjavax.persistence.ElementCollection;importjavax.persistence.Entity;importjavax.persistence.FetchType;importjavax.persistence.JoinColumn;importjavax.persistence.Table;importjavax.validation.constraints.Pattern;importjavax.validation.constraints.Size;importjava.util.List;@Entity@Table(name="destinations")publicclassDestinationextendsAbstractEntity{privatestaticfinallongserialVersionUID=1126074635410771215L;@NotEmpty(message="Destination name cannot be empty")@Size(min=2,max=100,message="Destination name must not be longer than 100 characters and shorter than 2 characters")@Pattern(regexp="[a-z-A-Z- ']*",message="Destination name has invalid characters")privateStringname;@NotEmpty(message="How to prepare when destination country is a total surprise?")privateStringcountry;privateStringdescription;@ElementCollection(fetch=FetchType.EAGER)@CollectionTable(name="destination_facts",joinColumns=@JoinColumn(name="destination",referencedColumnName="id"))@Column(name="fact")privateList<String>facts;/** Getters and setters used by unit and integration tests. */publicStringgetName(){returnthis.name;}publicStringgetCountry(){returnthis.country;}publicStringgetDescription(){returnthis.description;}publicList<String>getFacts(){returnthis.facts;}publicvoidsetName(Stringname){this.name=name;}publicvoidsetCountry(Stringcountry){this.country=country;}publicvoidsetFacts(List<String>facts){this.facts=facts;}publicvoidsetDescription(Stringdescription){this.description=description;}/** Default C-tor needed by Jackson JSON. */publicDestination(){}publicDestination(Stringname,Stringcountry){this.name=name;this.country=country;}publicDestination(Stringname,Stringcountry,List<String>facts,Stringdescription){this(name,country);this.facts=facts;this.description=description;}}
Additionally it inherits common attributes as: id (unique resource identifier), createdAt, modifiedAt from AbstractEntity. The last two are used in the context of database auditing: tracking and logging events related to all persistent entities.
“REST components communicate by transferring a representation of the data in a format matching one of the evolving set of standard data types.” - Fielding and Taylor. And by standard data types it is referred to a media type, something you would specify in Accept or Content-Type HTTP headers. Thus there is some magic glue needed to translate a Destination domain object to a json (JavaScript Object Notation) representation, for example.
Spring Data REST does that automatically for you by using Spring Data REST’s ObjectMapper, which has been specially configured to use intelligent serializers that can turn domain objects into links and back again. For JSON representation there are 2 such intelligent serializers supported in Spring Data REST: Jackson JSON and GSON. Jackson is being auto configured as default by Spring Boot. Spring Data REST’s ObjectMapper will try and serialize unmanaged beans as normal POJOs and it will try and create links to managed beans where that’s necessary. But if your domain model doesn’t easily lend itself to reading or writing plain JSON, you may want to configure Jackson’s ObjectMapper with your own custom type mappings and (de)serializers.
In my case, Spring Data REST’s ObjectMapper did the trick. I however have chosen to instruct Jackson JSON to simplify the view of my resources to not include null or empty attributes by using annotation @JsonInclude(JsonInclude.Include.NON_EMPTY) on AbstractEntity. The other excerpt was not including the auditing attributes, created and modified date, in the JSON representation: see @JsonIgnoreProperties({"createdAt", "modifiedAt"}) on AbstractEntity. The two date fields are initialized by application events that occur inside the persistence mechanism. As such the @PrePersist and @PreUpdate callback annotations will initialize the creation and modified timestamps of the API domain objects.
Coding the other two domain objects: Holiday and Agent is similar, you could try to do the exercise yourself based on the schema design or check the code listing here.
Coding the Repositories
The general idea of Spring Data REST is that builds on top of Spring Data repositories and automatically exports those as REST resources. I’ve created several repositories, one for each entity: DestinationRepository, HolidayRepository and AgentRepository. All repositories are Java interfaces extending from PagingAndSortingRepository interface, defined in Spring Data Commons, fact that ensures that common CRUD operations as: findAll, findOne, save, exists, count, delete, deleteAll will be inherited by all domain objects.
Listing 9 showcases the DestinationRepository which encapsulates various operations involving Destination objects. Spring Boot automatically spins up Spring Data JPA to create a concrete implementation of the DestinationRepository, at runtime, and configure it to talk to our back-end PostgreSQL database using JPA. @RepositoryRestResource annotation is not required for a repository to be exported. I’ve added it to explicitly, to document the export details, such as RESTful endpoints at: /destinations (the collection resource) and /destinations/{id} (the item resource).
By default, Spring Data REST will expose your CrudRepository using the name of the domain class (in this case Destination), lower-cased, and apply the Evo Inflector to pluralize this word.
I have also defined two custom queries to retrieve a list of Destination objects based on the country and find zero or one destination by name. Note that there is a UNIQUE database constraint on destination name. These two queries will be automatically exposed as RESTful endpoints: /destinations/search/findByCountry?country={country},
respectively: /destinations/search/findByName?name={name}
The listing of the HolidayRepository and AgentRepository interfaces can be found here.
Running the API
Ta da! 🎉 The screenshot above shows the back-end API in action, more specifically the /destinations endpoint.
There are many more RESTful endpoints exposed by the wanderlust service. I’ve created a test collection for other relevant endpoints and you can test them with Postman. The test collection can be downloaded here.
Manual testing can be a drag, and as a developer, we like to automate testing as much as possible. Listing below shows how to automate integration testing of HTTP operations on the Destination resource.
packagecristina.tech.blog.travel.domain;importorg.junit.Test;importorg.junit.runner.RunWith;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.web.client.TestRestTemplate;importorg.springframework.core.ParameterizedTypeReference;importorg.springframework.hateoas.Resource;importorg.springframework.http.HttpEntity;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.test.context.junit4.SpringRunner;importorg.springframework.web.util.UriComponentsBuilder;importjava.io.IOException;importjava.net.URI;importjava.util.ArrayList;importjava.util.List;importstaticorg.assertj.core.api.Assertions.assertThat;importstaticorg.springframework.http.HttpMethod.GET;importstaticorg.springframework.http.HttpMethod.PATCH;importstaticorg.springframework.http.HttpMethod.PUT;@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)publicclassWanderlustIntegrationTests{privatestaticfinalStringKOTOR_FACT="At 1300 metres deep, the Grand Canyon of Tara River is actually the deepest canyon in Europe and second largest in the world after the Colorado canyon in the USA.";privatestaticfinalStringKOTOR_DESCRIPTION="The gem of Montenegro";@AutowiredprivateTestRestTemplaterestTemplate;@Testpublicvoiddestinations()throwsIOException{DestinationdestinationKotor=newDestination("Kotor","ME");// POST, create destinationURIuri=restTemplate.postForLocation("/destinations",destinationKotor);// setup API response type to be a Destination HTTP ResourceParameterizedTypeReference<Resource<Destination>>responseType=newParameterizedTypeReference<Resource<Destination>>(){};// GET by URI, id as URI Path Param, i.e.: http://localhost:54294/destinations/1, assert Response HTTP code 200: OKResponseEntity<Resource<Destination>>getDestinationByURI=restTemplate.exchange(uri.toString(),GET,null,responseType);assertThat(destinationKotor.getCountry()).isEqualTo(getDestinationByURI.getBody().getContent().getCountry());assertThat(HttpStatus.OK.value()).isEqualTo(getDestinationByURI.getStatusCode().value());// PUT, replace destination, assert assert Response HTTP code 200: OKList<String>facts=newArrayList<>(1);facts.add(KOTOR_FACT);destinationKotor.setFacts(facts);ResponseEntity<Resource<Destination>>putDestination=restTemplate.exchange(uri.toString(),PUT,newHttpEntity<>(destinationKotor),responseType);assertThat(destinationKotor.getFacts()).isEqualTo(putDestination.getBody().getContent().getFacts());assertThat(getDestinationByURI.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value());// PATCH, update destination, does not work with exchange method, see: https://jira.spring.io/browse/SPR-15347destinationKotor.setDescription(KOTOR_DESCRIPTION);restTemplate.postForLocation(UriComponentsBuilder.fromUri(uri).queryParam("_method",PATCH.name()).build().toString(),destinationKotor);// GET by name, filter on destination name, assert return HTTP code 200ResponseEntity<Resource<Destination>>getDestinationByName=restTemplate.exchange(UriComponentsBuilder.fromPath("/destinations/search/findByName").queryParam("name","Kotor").build().toString(),GET,null,responseType);assertThat(destinationKotor.getDescription()).isEqualTo(getDestinationByName.getBody().getContent().getDescription());assertThat(getDestinationByName.getStatusCode().value()).isEqualTo(HttpStatus.OK.value());// DELETErestTemplate.delete(uri.toString());// GET by country, assert destination does not exist anymoreResponseEntity<Resource<Destination>>getDestinationByCountry=restTemplate.exchange(UriComponentsBuilder.fromPath("/destinations/search/findByCountry").queryParam("country","ME").build().toString(),GET,null,responseType);assertThat(getDestinationByCountry.getBody().getContent().getId()).isEqualTo(null);}}
HAL and Resource discoverability
A core principle of HATEOAS (Hypermedia as the Engine of Application State) is that resources should be discoverable through the publication of links that point to the available resources. There are a few competing de-facto standards of how to represent links in JSON. By default, Spring Data REST uses HAL to render responses. HAL defines links to be contained in a property of the returned document.
If you can remember from Listing 1: build.gradle, we have added a Gradle runtime dependency for browsing Spring Data REST repositories in your Web browser. It is an API browser for the hal+json media type.
When you point your browser to location: http://localhost:9000/api/opentravel/, you should see something similar to below. The HAL Browser makes for quite a nice Wanderlust REST API documentation, by listing: links, headers, URI templates, response body.
RESTing in the APLS
“ALPS is a data format for defining simple descriptions of application-level semantics, similar in complexity to HTML microformats. An ALPS document can be used as a profile to explain the application semantics of a document with an application-agnostic media type (such as HTML, HAL, Collection+JSON, Siren, etc.). This increases the reusability of profile documents across media types.” - M. Admundsen / L. Richardson / M. Foster
You can find the full code base of my Incubator Wanderlust OpenTravel API at my public GitHub repo.
Feel free to clone the project and give wanderlust-api a spin!
REpresentational State Transfer (REST) is an architectural style inspired by the Web and nowadays Hypermedia-driven CRUD REST services are at the backbone of a microservice internal architecture.
Benefits as scalability, ease of testing and deployment, as well as the chance to eliminate long-term commitment to a single technology stack, have convinced some big enterprise players
– like Amazon, Netflix,
eBay,
Spotify
– to begin transitioning their monolithic applications to microservices.
Purpose
If we want to accelerate microservices adoption rather than code at monolithic applications, it’s essential that we can create a basic service with a minimum of effort.
Writing a REST-based microservice that accesses a data model typically involves writing the Data Access Objects (DAO) to access the data repository and writing the REST methods independently.
This often means that you are responsible for writing all of the SQL queries or JPQL(Java Persistence Query Language).
The Spring Data REST framework provides you with the ability to create data repository implementations automatically at runtime,
from a data repository interface, and expose these data repositories over HTTP as a REST service.
It takes the features of Spring HATEOAS and Spring Data JPA and combines them automatically.
This guide will give you a high-level walk through of my process of creating a microservice - called Wanderlust - that exposes a discoverable REST API of a simple open-travel domain model using JSON HAL as media type.
Minimum Viable Product
list, search by country or name travel destinations
create, validate, update and delete a travel destination
list, search travel agents by name
create, validate, update and delete a travel agent
list, search holiday packages by destination country
create, validate a holiday package for a travel destination
pagination and sorting for list and search operations
cURL 7.0+ (cURL is installed by default on most UNIX and Linux distributions. To install cURL on a Windows 64-bit machine,
click here) or Postman
Your favorite Java code editor. At the moment my favorite one is IntelliJ IDEA
When running on OS X, installion steps can be found in the project’s README.
“Bootiful Wanderlust” with Spring Boot and Spring Initializr
Sometimes the hardest part of any project is getting started. You have to setup a directory structure for various project artifacts, create a build file and populate it with all library dependencies. This is where Spring Initializr comes into handy. Spring Initializr is ultimately a web application that can generate a Spring Boot project structure for you. It doesn’t generate any application code, but it will give you a basic project structure and either a Maven or a Gradle build specification to build your code with. All you need to do is write the application code.
Spring Boot, on the other hand, helps package the autonomous data microservice application as an uber-self-runnable-JAR (Java ARchive) with an embedded application container (Apache Tomcat - autoconfigured by default, Jetty, JBoss Undertow). Spring Boot also became “de-facto” way to create Spring applications.
Why Spring Boot?
It’s suitable for Cloud Native Applications that follow the 12factor patterns (developed by the Netflix engineering team)
Productivity increases by reducing time of development and deployment
Enterprise-production-ready Spring applications
Non-functional requirements, such as Spring Boot Actuator (a module that brings metrics, health checks and management features)
The most straightforward way to use the Spring Initializr is to point your web browser to http://start.spring.io. Fill in basic project information on the left side of the form, search and add dependencies listed in the figure below on the right side of the form and you should be all set to download the project template.
The Generate Project button will download the template project.
“REST-Assured”: Building Continuous Delivery confidence with Test Driven Development
Before even starting to write a line of code, it is a good practice to think about the approach to take to automate code quality checks so that you can code and refactor with confidence. And at the same time strive to minimize the number of defects!
While I will show you how to fully unit and integration test the back-end API in a later post, I’ll start by adding the tooling that calculates the percentage of code accessed by tests to the project build specification file (build.gradle).
The goal is to have each time when building the project, JaCoCo (Java Code Coverage) produce an XML report in the build directory: wanderlust-open-travel-api/build/reports/jacoco/test/jacocoTestReport.xml to be used with the Continuous Integration (CI) server of your choice - for me that is Travis - to visually quantify test coverage and its evolution between chronological software increments.
Go ahead and open the project template structure generated with Spring Initializr in your favorite IDE and add the jacoco and coveralls dependencies to the Gradle Build specification. The result should look similar to the listing below.
The Gradle IDEA plugin - apply plugin: 'idea' - generates files for working with the project in IntelliJ IDEA. To generate the project files, simply open a terminal window, navigate to the project root directory and run the idea task.
Cristinas-iMac:wanderlust-open-travel-api cristina$ ./gradlew idea
:ideaModule
:ideaProject
:ideaWorkspace
:idea
Creating and Initializing PostgreSQL Relational Data Store with Flyway and SQL
One of the design principles of microservices architecture is to have a separate data store for each microservice. If you got so far, you should already have a PostgreSQL data store instance running on your machine with a created database named wanderlust, as well as USERNAME_POSTGRES and PWD_POSTGRES environment variables configured and resolvable according to the project’s README
To check whether your connection to the database is working properly, you can open a command line tool and type in:
psql -h localhost wanderlust
You should see an output similar with to below:
psql (9.5.3)
Type "help" for help.
wanderlust=# help
You are using psql, the command-line interface to PostgreSQL.
Once that is working, we need to instruct Spring Boot to use PostgreSQL for storing the microservice data. As you might already know, Spring Boot is using by default H2 in-memory database. In-memory databases are useful in the early development stages in local environments, but they have lots of restrictions. As the development progresses, you would most probably require a persistent data store to develop and test your application before deploying it to use a production database server. Spring Boot integrates with all major RDBMS and NoSQL databases as: MySQL, PostgreSQL, Oracle, MongoDB, Cassandra, even Microsoft SQL Server. Isn’t Microsoft taking major steps towards open source? :)
To use PostgreSQL, you will need the proper database drivers. The required JARs should already be provisioned due to the fact that we have included it as a dependency with Spring Initializr. Furthermore we would like to use PostgreSQL with a lightweight, high-performance JDBC connection pool technology like HikariCP to prepare the service for production-grade environment concurrency.
This last one can be added to build.gradle file:
Additionally we will check that Spring Boot’s AutoConfiguration feature has yielded the proper connection properties and tweak them accordingly.
INSERTINTOdestinationsVALUES((SELECTnextval('destinations_id_seq')),'Quintessential Japan','Japan','Life-changing-experience');INSERTINTOdestination_factsVALUES((SELECTcurrval('destinations_id_seq')),'There are over 5.5 million vending machines in Japan selling everything from umbrellas and cigarettes to canned bread and hot noodles.'),((SELECTcurrval('destinations_id_seq')),'Japan''s birth rate has plummeted so significantly that adult nappies (diapers) outsell babies'' nappies, which are also sold in vending machines.'),((SELECTcurrval('destinations_id_seq')),'It is estimated that more paper is used for manga comics than for toilet paper in Japan. (Surprise: both are sold in vending machines as well.)'),((SELECTcurrval('destinations_id_seq')),'One of the world''s most famous pilgrimage routes after the Camino de Santiago is Japan''s Kumano Kodo near Osaka.');INSERTINTOtravel_agents(id,name,country,postal_code,email,website)VALUES((SELECTnextval('travel_agents_id_seq')),'Shoestring','NL','1114 AA','info@shoestring.nl','https://shoestring.nl');INSERTINTOholiday_packages(id,destination,depart_from,package_info)VALUES((SELECTnextval('holiday_packages_id_seq')),1,'Amsterdam Schipol','New horizons in the land of the rising sun');INSERTINTOtravel_agent_holiday_packagesVALUES((SELECTcurrval('travel_agents_id_seq')),(SELECTcurrval('holiday_packages_id_seq')));
Database scripts will be run automatically by Flywaydb upon service start. Before starting the service, I will change the default server port. Spring Boot default is 8080 and it supplies you with the server.port property for configuring the port on which our Spring Boot application should run.
You may be wondering: Wait a minute, what is this bootRun command? How can I install it? If you revisit Listing 1: build.gradle, line
apply plugin: 'org.springframework.boot' provides Spring Boot support in Gradle. The Spring Boot Gradle Plugin allows you to package executable jar or war archives. The default option for packaging in Spring Initializr is jar (Java ARchive) and that is also reflected in Listing 1: build.gradle
Listing 6 shows you the entry point for a Spring Boot application in Java. The main() method uses Spring Boot’s SpringApplication.run() method to launch an application. Did you notice that there wasn’t a single line of XML? No web.xml file either. This web application is 100% pure Java and you didn’t have to deal with configuring any plumbing or infrastructure. The run() method call accepts two parameters - the class annotated with @SpringBootApplication and the application’s arguments.
You can check the schema version and installation details via Spring Actuator endpoint or command line:
Flywaydb is a great tool that makes database migrations automation easy. It deserves in itself a separate blog post. I particularly like this one!
Coding the Domain Model
The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a travel destination, a holiday package, a tour operator/agent.
“In other words, any concept that might be the target of an author’s hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time.” - Roy Fielding’s dissertation
In this blog post I will exemplify Destination class which is a simple model of a travel destination resource, with four basic attributes: the name, country, description and off course some funny facts about the destination. Geodata (GIS) and photos anyone? ;) It is annotated with @Entity, indicating that it is a JPA entity, mapping to persistent storage: wanderlust.destinations and wanderlust.destination_facts schema tables.
The Destination class encapsulates validation constraints with the use of Hibernate Validator@NotEmpty and
JSR 303/349 Bean Validation@Size,@Pattern annotations. As you can see the name and the country of the destination are mandatory fields.
I have used the message attribute of the bean validation annotations to provide API clients with descriptive validation error messages.
packagecristina.tech.blog.travel.domain;importorg.hibernate.validator.constraints.NotEmpty;importjavax.persistence.CollectionTable;importjavax.persistence.Column;importjavax.persistence.ElementCollection;importjavax.persistence.Entity;importjavax.persistence.FetchType;importjavax.persistence.JoinColumn;importjavax.persistence.Table;importjavax.validation.constraints.Pattern;importjavax.validation.constraints.Size;importjava.util.List;@Entity@Table(name="destinations")publicclassDestinationextendsAbstractEntity{privatestaticfinallongserialVersionUID=1126074635410771215L;@NotEmpty(message="Destination name cannot be empty")@Size(min=2,max=100,message="Destination name must not be longer than 100 characters and shorter than 2 characters")@Pattern(regexp="[a-z-A-Z- ']*",message="Destination name has invalid characters")privateStringname;@NotEmpty(message="How to prepare when destination country is a total surprise?")privateStringcountry;privateStringdescription;@ElementCollection(fetch=FetchType.EAGER)@CollectionTable(name="destination_facts",joinColumns=@JoinColumn(name="destination",referencedColumnName="id"))@Column(name="fact")privateList<String>facts;/** Getters and setters used by unit and integration tests. */publicStringgetName(){returnthis.name;}publicStringgetCountry(){returnthis.country;}publicStringgetDescription(){returnthis.description;}publicList<String>getFacts(){returnthis.facts;}publicvoidsetName(Stringname){this.name=name;}publicvoidsetCountry(Stringcountry){this.country=country;}publicvoidsetFacts(List<String>facts){this.facts=facts;}publicvoidsetDescription(Stringdescription){this.description=description;}/** Default C-tor needed by Jackson JSON. */publicDestination(){}publicDestination(Stringname,Stringcountry){this.name=name;this.country=country;}publicDestination(Stringname,Stringcountry,List<String>facts,Stringdescription){this(name,country);this.facts=facts;this.description=description;}}
Additionally it inherits common attributes as: id (unique resource identifier), createdAt, modifiedAt from AbstractEntity. The last two are used in the context of database auditing: tracking and logging events related to all persistent entities.
“REST components communicate by transferring a representation of the data in a format matching one of the evolving set of standard data types.” - Fielding and Taylor. And by standard data types it is referred to a media type, something you would specify in Accept or Content-Type HTTP headers. Thus there is some magic glue needed to translate a Destination domain object to a json (JavaScript Object Notation) representation, for example.
Spring Data REST does that automatically for you by using Spring Data REST’s ObjectMapper, which has been specially configured to use intelligent serializers that can turn domain objects into links and back again. For JSON representation there are 2 such intelligent serializers supported in Spring Data REST: Jackson JSON and GSON. Jackson is being auto configured as default by Spring Boot. Spring Data REST’s ObjectMapper will try and serialize unmanaged beans as normal POJOs and it will try and create links to managed beans where that’s necessary. But if your domain model doesn’t easily lend itself to reading or writing plain JSON, you may want to configure Jackson’s ObjectMapper with your own custom type mappings and (de)serializers.
In my case, Spring Data REST’s ObjectMapper did the trick. I however have chosen to instruct Jackson JSON to simplify the view of my resources to not include null or empty attributes by using annotation @JsonInclude(JsonInclude.Include.NON_EMPTY) on AbstractEntity. The other excerpt was not including the auditing attributes, created and modified date, in the JSON representation: see @JsonIgnoreProperties({"createdAt", "modifiedAt"}) on AbstractEntity. The two date fields are initialized by application events that occur inside the persistence mechanism. As such the @PrePersist and @PreUpdate callback annotations will initialize the creation and modified timestamps of the API domain objects.
Coding the other two domain objects: Holiday and Agent is similar, you could try to do the exercise yourself based on the schema design or check the code listing here.
Coding the Repositories
The general idea of Spring Data REST is that builds on top of Spring Data repositories and automatically exports those as REST resources. I’ve created several repositories, one for each entity: DestinationRepository, HolidayRepository and AgentRepository. All repositories are Java interfaces extending from PagingAndSortingRepository interface, defined in Spring Data Commons, fact that ensures that common CRUD operations as: findAll, findOne, save, exists, count, delete, deleteAll will be inherited by all domain objects.
Listing 9 showcases the DestinationRepository which encapsulates various operations involving Destination objects. Spring Boot automatically spins up Spring Data JPA to create a concrete implementation of the DestinationRepository, at runtime, and configure it to talk to our back-end PostgreSQL database using JPA. @RepositoryRestResource annotation is not required for a repository to be exported. I’ve added it to explicitly, to document the export details, such as RESTful endpoints at: /destinations (the collection resource) and /destinations/{id} (the item resource).
By default, Spring Data REST will expose your CrudRepository using the name of the domain class (in this case Destination), lower-cased, and apply the Evo Inflector to pluralize this word.
I have also defined two custom queries to retrieve a list of Destination objects based on the country and find zero or one destination by name. Note that there is a UNIQUE database constraint on destination name. These two queries will be automatically exposed as RESTful endpoints: /destinations/search/findByCountry?country={country},
respectively: /destinations/search/findByName?name={name}
The listing of the HolidayRepository and AgentRepository interfaces can be found here.
Running the API
Ta da! 🎉 The screenshot above shows the back-end API in action, more specifically the /destinations endpoint.
There are many more RESTful endpoints exposed by the wanderlust service. I’ve created a test collection for other relevant endpoints and you can test them with Postman. The test collection can be downloaded here.
Manual testing can be a drag, and as a developer, we like to automate testing as much as possible. Listing below shows how to automate integration testing of HTTP operations on the Destination resource.
packagecristina.tech.blog.travel.domain;importorg.junit.Test;importorg.junit.runner.RunWith;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.web.client.TestRestTemplate;importorg.springframework.core.ParameterizedTypeReference;importorg.springframework.hateoas.Resource;importorg.springframework.http.HttpEntity;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.test.context.junit4.SpringRunner;importorg.springframework.web.util.UriComponentsBuilder;importjava.io.IOException;importjava.net.URI;importjava.util.ArrayList;importjava.util.List;importstaticorg.assertj.core.api.Assertions.assertThat;importstaticorg.springframework.http.HttpMethod.GET;importstaticorg.springframework.http.HttpMethod.PATCH;importstaticorg.springframework.http.HttpMethod.PUT;@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)publicclassWanderlustIntegrationTests{privatestaticfinalStringKOTOR_FACT="At 1300 metres deep, the Grand Canyon of Tara River is actually the deepest canyon in Europe and second largest in the world after the Colorado canyon in the USA.";privatestaticfinalStringKOTOR_DESCRIPTION="The gem of Montenegro";@AutowiredprivateTestRestTemplaterestTemplate;@Testpublicvoiddestinations()throwsIOException{DestinationdestinationKotor=newDestination("Kotor","ME");// POST, create destinationURIuri=restTemplate.postForLocation("/destinations",destinationKotor);// setup API response type to be a Destination HTTP ResourceParameterizedTypeReference<Resource<Destination>>responseType=newParameterizedTypeReference<Resource<Destination>>(){};// GET by URI, id as URI Path Param, i.e.: http://localhost:54294/destinations/1, assert Response HTTP code 200: OKResponseEntity<Resource<Destination>>getDestinationByURI=restTemplate.exchange(uri.toString(),GET,null,responseType);assertThat(destinationKotor.getCountry()).isEqualTo(getDestinationByURI.getBody().getContent().getCountry());assertThat(HttpStatus.OK.value()).isEqualTo(getDestinationByURI.getStatusCode().value());// PUT, replace destination, assert assert Response HTTP code 200: OKList<String>facts=newArrayList<>(1);facts.add(KOTOR_FACT);destinationKotor.setFacts(facts);ResponseEntity<Resource<Destination>>putDestination=restTemplate.exchange(uri.toString(),PUT,newHttpEntity<>(destinationKotor),responseType);assertThat(destinationKotor.getFacts()).isEqualTo(putDestination.getBody().getContent().getFacts());assertThat(getDestinationByURI.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value());// PATCH, update destination, does not work with exchange method, see: https://jira.spring.io/browse/SPR-15347destinationKotor.setDescription(KOTOR_DESCRIPTION);restTemplate.postForLocation(UriComponentsBuilder.fromUri(uri).queryParam("_method",PATCH.name()).build().toString(),destinationKotor);// GET by name, filter on destination name, assert return HTTP code 200ResponseEntity<Resource<Destination>>getDestinationByName=restTemplate.exchange(UriComponentsBuilder.fromPath("/destinations/search/findByName").queryParam("name","Kotor").build().toString(),GET,null,responseType);assertThat(destinationKotor.getDescription()).isEqualTo(getDestinationByName.getBody().getContent().getDescription());assertThat(getDestinationByName.getStatusCode().value()).isEqualTo(HttpStatus.OK.value());// DELETErestTemplate.delete(uri.toString());// GET by country, assert destination does not exist anymoreResponseEntity<Resource<Destination>>getDestinationByCountry=restTemplate.exchange(UriComponentsBuilder.fromPath("/destinations/search/findByCountry").queryParam("country","ME").build().toString(),GET,null,responseType);assertThat(getDestinationByCountry.getBody().getContent().getId()).isEqualTo(null);}}
HAL and Resource discoverability
A core principle of HATEOAS (Hypermedia as the Engine of Application State) is that resources should be discoverable through the publication of links that point to the available resources. There are a few competing de-facto standards of how to represent links in JSON. By default, Spring Data REST uses HAL to render responses. HAL defines links to be contained in a property of the returned document.
If you can remember from Listing 1: build.gradle, we have added a Gradle runtime dependency for browsing Spring Data REST repositories in your Web browser. It is an API browser for the hal+json media type.
When you point your browser to location: http://localhost:9000/api/opentravel/, you should see something similar to below. The HAL Browser makes for quite a nice Wanderlust REST API documentation, by listing: links, headers, URI templates, response body.
RESTing in the APLS
“ALPS is a data format for defining simple descriptions of application-level semantics, similar in complexity to HTML microformats. An ALPS document can be used as a profile to explain the application semantics of a document with an application-agnostic media type (such as HTML, HAL, Collection+JSON, Siren, etc.). This increases the reusability of profile documents across media types.” - M. Admundsen / L. Richardson / M. Foster
You can find the full code base of my Incubator Wanderlust OpenTravel API at my public GitHub repo.
Feel free to clone the project and give wanderlust-api a spin!