Building a RESTful API with Java and Spring Framework — Part 1

Nowadays, it is more popular to have online applications in all environments. These applications aim to consume information through interfaces with a series of routines and standards. We named these types of applications of API.

The API acronym comes from Application Programming Interface. It is a set of documented standards and rules so that an application X can use another application Y features without knowing the application X implementation details.

Is it still confusing? To a better understanding, let’s imagine a day in a company that has an e-commerce website. The developers are working on the solution based on the store’s dynamics, such as creating, updating, deleting products internally, and showing the products to the customers online. These resources can be developed in a server-side application as if it were an API. Then, the e-commerce website and the company’s other applications can use this information.

Now that we already know what an API is and where it can be used, we need to understand how everything works. The next section presents the protocols used for communication between applications and how the data can be represented.

Are HTTP and REST the same thing?

The primary communication protocol on the Web is HTTP. It works like a request-response protocol in a model we call a client-server. In our e-commerce example, the browser used to access the website would be the client — the computer or virtual machine in some cloud service where the server host the API. The client sends an HTTP request to the server, and the server, with its resources and content, returns a response message to the client.

Example of how the HTTP protocol works

The HTTP protocol has been used since 1990, and your current version is described in this draft is the HTTP/3. Eight methods are defined that determine actions to be performed when requesting a resource from the server. Of these eight, the four most used are:

  • GET: the method used to read and retrieve data. Request a representation of the specified resource and return that representation.
  • POST: the method used to create a new resource and sends data to the server. The Content-Type in the header indicates the type of the request body.
  • PUT: creates a new resource or replaces a representation of the target resource with the latest data. PUT, and POST, the difference is that it is idempotent. When calling it one or more times repeatedly, the effect is the same, while getting POST can have additional consequences. For example, if we create a product with POST, if the URL defined in the API is called 20 times, we will create 20 items, and each of these items will have a different ID. In the PUT method, if you execute 20 times the URL defined in the API, the result has to be the same: the same item is updated 20 times.
  • DELETE: deletes the resource.

Based on these methods, the server must process each request and return an appropriate response. The response format can be XML, JSON, YAML, text, among others. Plus, these responses are separated into five groups:

  • 1XX — General information
  • 2XX — Success
  • 3XX — Redirect
  • 4XX — Client error
  • 5XX — Server error

But isn’t that REST? No, it’s not. REST, an acronym for Representational State Transfer, is an abstraction of the architecture detail above. It is a software architecture style that defines a series of restrictions for creating web services. That is, it restricts how its components interact. This term was introduced and explained by Roy Fielding in his doctoral thesis in the late 90s and early 2000s.

In the thesis, Fielding defined the REST principles known as the HTTP object model. It started to be used to design HTTP 1.1 and URI (Uniform Resource Identifiers) standards. Thus, we can say that in its semantics, REST uses HTTP methods. Besides, it is also worth remembering that a REST service must be Stateless: every request must be self-sufficient. That is, each request is a different and independent request. There must be no way in the request to save the information’s status.

So, what does it mean to be RESTful? An API is RESTful if we guarantee that the implementation is following this REST architecture. Conceptually, in RESTful, both data and functionality are considered resources accessible to customers URIs. These URIs are usually web addresses to identify the server on which the application is hosted. Furthermore, it represents the application itself and which of its resources are being requested.

That way, exposing your API (or your API services, if you prefer) in RESTful means that the REST principles and their restrictions apply to you. After laying our cards on the table, let’s get our hands dirty! Besides the use of Java (version 11) with Spring Framework as the API basis, we will use as tools:

  • Apache Maven (for dependencies management)
  • Postman (for the execution of API tests and requests in general)
  • JUnit5 (for unit and integration tests)
  • Lombok (to reduce boilerplate code)
  • Log4j (for application logging)
  • TravisCI (for continuous integration)

All the code produced in this article, a simplified API version, can be accessed here:

Suppose you want to skip the tutorial. The final code, an optimized version, applying best practices, can be accessed in this repository on Github:

Getting started with the RESTful API

The API should create, update, delete, and list travels. Also, it must calculate statistics on the trips created. Note: at this point, we will not deal with the persistence layer. So, technically, the Travel entity will contain an explicit field called id. The API will have the following endpoints:

POST/api-travels/travels: creates a trip.

Request Body:

{   
"id": 1,
"orderNumber": "220788",
"amount": "22.88",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}

Where:

  • id: unique travel number.
  • orderNumber: identification number of a trip on the system.
  • amount: travel amount; it must be a string of arbitrary length that can be parsed as a BigDecimal.
  • startDate: travel starts date-time in the ISO 8601 format YYYY-MM-DDThh:mm:ss.sssZ in the Local time zone.
  • endDate: end date of the trip in the ISO 8601 format YYYY-MM-DDThh:mm:ss.sssZ in the Local time zone.
  • type: ONE-WAY, RETURN, or MULTI-CITY.

You must return with an empty body with one of the following codes:

  • 201: if successfully created.
  • 400: if the request JSON is invalid.
  • 422: if any of the request fields are not parsable or the start date is greater than the end date.

PUT/api-travels /travels/{id}: update a trip.

Request Body:

{   
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}

The object to be modified must be sent. The return must be the object itself.

{   
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}

The response must contain the following codes:

  • 200: if successfully updated.
  • 400: if the request JSON is invalid.
  • 404: if you try to update a record that doesn’t exist.
  • 422: if any of the fields is not parsable (JSON is poorly formatted).

GET/api-travels/travels: returns all travels created.

A list of travels must be returned.

{   
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
},
{
"id": 2,
"orderNumber": "300691",
"amount": "120.0",
"startDate": "2019–09–11T10:22:30.312Z",
"type": "ONE-WAY"
}

The response must contain the following codes:

  • 200: if there are travels created.
  • 404: if there are no travels created.

DELETE/api-travels/travels: remove all travels.

You must accept a request with an empty body and return 204.

GET/api-travels/statistics: returns basic statistics about the travels created.

{   
"sum": "150.06",
"avg": "75.3",
"max": "120.0",
"min": "30.06",
"count": "2"
}

Where:

  • sum: a BigDecimal with the total of the travels.
  • avg: a BigDecimal with the average of the values of the travels.
  • max: a BigDecimal with the highest value among the created travels.
  • min: a BigDecimal with the lowest value among the travels.
  • count: a Long with of the total number of travels.

All fields that are BigDecimal must have only two decimal places. E.g., 15.385 must be returned as 15.39.

Once we detailed the features that we need to implement, let’s work!

1) The first step is to create a Spring Boot project in Spring Initializr.

Example of creating a Spring Boot project by Spring Initializr

As dependencies, we will select Spring Web (Spring MVC) and Lombok. Spring MVC is a framework that helps develop web applications in the MVC standard (model-view-controller). Lombok is a Java library focused on productivity and code reduction through annotations that teach the compiler how to create and manipulate Java code. That is, you will no longer need to write methods such as getter, setter, equals, hashCode, and constructors.

2) With the project created, we will create the model, controller, and service packages.

Example of creating project packages

Next, we will create the models: Travel and Statistic.

Example of the Travel class of the model package
Example of the Statistic class of the model package

3) With the models created, we will add the API services layer: TravelService and StatisticService.

The class TravelService must contain seven methods to meet the lifecycle requirements of this service in the API:

  • isJSONValid: to verify that the JSON is valid.
  • parseId: to parse the JSON id field.
  • parseAmount: to parse the amount field of JSON.
  • parseStartDate: to parse the JSON startDate field.
  • parseEndDate: to parse the JSON startDate field.
  • isStartDateGreaterThanEndDate: to verify that the start date is greater than the end date.
  • create: to create a trip (travel).
  • update: to update a trip (travel).
  • add: to add a trip (travel) to the list.
  • findById: to retrieve a trip (travel) by id.
  • find: to retrieve all created travels.
  • delete: remove travels.

The class StatisticService should contain only the method for creating API statistics: createStatistics.

4) Next step, we will create the routes in the controllers: TravelController (for routes related to travels) and StatisticController (for routes related to statistics).

The TravelController should contain all the operations detailed above: create a trip (POST), update a trip (PUT), list all travels (GET), and remove all travels (DELETE).

Once the routes are created, let’s go up the API with the command:

mvn spring-boot:run

It is also possible to run the API through your IDE as a Java Application. The result of both commands should be similar to the figure below.

Spring Boot initializing the application

By default, the API will be available at http://localhost:8080/. With the API working, we can now test the routes. For the initial tests, I used Postman and, as a reference, the examples showed in the section Building the API.

POST/api-travels/travels via Postman

Example of a POST done via Postman

PUT/api-travels/travels/1 via Postman

Example of a PUT done via Postman

GET/api-travels/travels via Postman

Example of a GET done via Postman

DELETE/api-travels/travels via Postman

Example of a DELETE done via Postman

Also, the StatisticController should contain the detailed statistics operation mentioned at the beginning of the section.

Statistics controller example

GET/api-travels/statistics via Postman

Example of a GET on the statistics route, done via Postman

How to test your API easily and quickly?

How to guarantee quality in the API routes? In this tutorial, I used JUnit5 to create unit tests and Spring Boot’s TestRestTemplate to create integration tests. For both tests running correctly, we need to add some plugins to our: maven-failsafe-plugin and build-helper-maven-plugin. The configuration should be the same as below:

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<trimStackTrace>false</trimStackTrace>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId
<executions>
<execution>
<id>add-integration-test-sources</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/it/java</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-integration-test-resources</id>
<phase>generate-test-resources</phase>
<goals>
<goal>add-test-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/it/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>

Also, we will create two packages: src/test/java for unit testing. The goal here is to validate the business logic implemented in services and controllers; and src/it/java with the TravelsJavaApiIntegrationTest class for integration testing. The goal here is to validate the integration and flow of the API routes.

Example of the TravelsJavaApiUnitTests unit test class
TravelsJavaApiIntegrationTest integration test class example

To perform only unit tests, use the command:

mvn test

To perform both tests, use the command:

mvn integration-test

The result should be similar to below:

Example of tests execution with Maven

The last step is to create continuous integration with the tests ready. That means to make the API’s evolution and quality regularly monitored, maintaining integration with the code developed. In the next section, we will use TravisCI to create this behavior in our ecosystem.

Always build to always conquer!

The first step to enable the continuous integration workflow with TravisCI is to create a .travis.yml file, in the project’s root path, with the following configuration:

In this file, we need to configure the virtual machine that will boot our app. The main configurations are:

  • SO distribution: we use Linux, Ubuntu (14.04).
  • Language: Java
  • Sudo: option to run with (true) or without (false) root permission
  • JDK or Java version: we use Java/JDK 11.
  • Git: how to download the source code. For example, the number of commits considered (up to 2 commits).
  • Script: how to perform the package installation and the tests.

Alongside, you need to enable the API flow in the TravisCI dashboard:

Guaranteed that everything is correct, as soon as the project has an update, the main branch’s build is triggered automatically.

The expected result is that the build is successful.

By checking the build log, we can see that the branch was built and the tests performed successfully.

Finally, our API is ready and validated!

The topic addressed here was an introduction to Java and Spring. If you have improvements to make, please fork the travels-api project on Github and code!

At the end of the series, the code will have best practices recommended in developing an API, such as versioning, HATEOAS, Spring Error Handling, and other topics. Hence, If you are ready for these topics, go ahead and look at the travels-java-api project and enjoy it! If you have any further questions, please, contact me.

I hope you enjoyed it! See you in the next post!

Senior Software Developer/Tech Lead, master in Computer Science/Software Engineering, Java, open source, and software quality enthusiast.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store