A walk through a REST API with Java

Tiago Albuquerque
10 min readNov 15, 2020

A guided journey using Spring Boot

In this article it will be described the steps of building a REST API with Java, and talk about some design decisions and personal preferences. I built an API to serve as a reference, including authentication/authorization using JWT and all sort of tests.

I’ll try to put into words what I’ve been studying and applying about REST APIs, and of course, talking about each point with my personal bias.

For reference, the technologies used in the project were: Java 11, Spring Boot, Spring Web, Spring Security, Spring Data, H2 database, Json Web Token, mapstruct, rest-assured and cucumber.

First of all, some theory

REST (REpresentational State Transfer) is an software architectural style for providing standards between systems on the web, making it easier for them to communicate with each other. REST was defined by Roy Fielding in his 2000 PhD dissertation.

By using REST architectural style it is possible to separate the concerns of client and server, which can be developed independent of each other.

This article assumes previous basic knowledge about REST architecture. Nevertheless, let's review some key definitions:

Resource is anything on the Web that can be identified, named or addressed by an URI (Uniform Resource Identifier). The URL (Uniform Resource Locator) of the resource is also know as its endpoint. The operations (or state transitions) on a given resource are performed using resource methods; and when it is used the HTTP protocol they are related to HTTP Methods (or Verbs). The most common used HTTP methods are GET, POST, PUT, DELETE for retrieve, create, update or delete an identified resource. A architectural API that complies to all REST specifications and constraints are know as Restful APIs.

In summary, when a REST endpoint receives a request, the server will transfer to the client a representation of the state of the that resource.

There are six guiding architectural constraints that defines a RESTful system, which provides this desired properties of performance, scalability, simplicity, visibility, portability, modifiability and reliability:

  • Client-Server: this constraints is associated with separation of concerns, so the client and server can evolve independently. The interaction between them is by requests initiated by the client and server responses to that requests.
  • Stateless: it means that there is no “client-session” in the server, so no client context are stored between requests. Every request contains all the information that allows the server perform the operation and return a response.
  • Cacheable: cache requires that the data within a response to a request to be labeled (implicitly or explicitly) as cacheable or non-cacheable. So, for a cacheable response, the client can manage the data to prevent unnecessary new requests to the server, reusing previous response data (if not expired).
  • Layered System: between the client request and the server response, there can exist many (or none) intermediary servers/layers, which can handle load balancing, cache and security logic for instance. The point here is that the client does not have to know how many layers or server are there to generate a response from a resource request.
  • Uniform Interface: this constraint simplifies and decouples the architecture, so each part act independently. There are four inner constraints for this uniform interface as follows: a) Resource identification in requests (a request has to include a resource identifier); b) Resource manipulation through representations (response includes metadata about the resource so the client can modify the resource); c) Self-descriptive messages (each request or response message contains all the information that is needed to be understood); d) Hypermedia As The Engine Of Application State - HATEOAS (server responses provide links dynamically to discover all the available resources it needs).
  • Code on Demand: this constraint is optional and means that the server can transfer executable code to client, usually in form of scripts.

What is the project about?

For the purpose of this article, it will be used a simple Time Tracker HTTP API implementation as example, where I intend to go through each aspect of it and explain the design decisions.

Simply put, the application is a daily time tracker, where users can input time entries to track how they spent their time. Each time entry is associated to a category defined by the user.

Users are registered in the system by an Admin User. The user can manage the categories he is associated with and sent time entries.

By the description above, we can identify some resources for the API: ‘User’, ‘Category’ and ‘Time Entry’.

Some operations will only be allowed for admin users, whose authorization we will implement later. There will be an endpoint to authenticate the user and generate jwt tokens to access the API.

First, I like to make everything works fine and then apply the security rules to get the job done.

I'll try to go through the implementation layers step-by-step, but the complete project is available on GitHub here.

Creating the project

First, let's use the Spring Initializer to create our Spring Boot project. I chose a maven based project with Java 11 and Spring dependencies: spring-web, spring-test, spring-data, spring-validation, spring-hateoas and spring- security. After, it was added the H2, mapstruct and jjwt dependencies.

Spring Initialzr project setup

General view of generated project in the IDE (I'm using IntelliJ IDEA):

Imported project in IDE

Dependencies section of pom.xml will be as follows:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>

Implementing Model classes

Since this is a quite simple project, let's use layered packaging.

So, let's implement the model classes (User, Category and TimeEntry) into ‘model’ package, which are plain java classes like this:

model package
package br.com.tiagoamp.timetracker.model;
import java.util.List;
public class User {
private Long id;
private String email;
private String name;
private String password;
private List<Category> categories;
private List<TimeEntry> timeEntries;
private Role role;
// getter's and setter's
}
package br.com.tiagoamp.timetracker.model;
public class Category {
private Integer id;
private String name;
private String description;
// getter's and setter's
}
package br.com.tiagoamp.timetracker.model;
import java.time.LocalDateTime;
public class TimeEntry {
private Long id;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Category category;
private String annotations;
// getter's and setter's
}

Creating REST Controllers

Once defined the model, I like to create the REST Controller classes for our resources, so to get forced to think of what operations the API will provide and which endpoints will be necessary. Sometimes, just to make it work, it can be implemented mock responses for each candidate endpoint just to validate them.

The Controllers classes, in my opinion, should be only responsible for accept and validate request data, call business services classes and encapsulate resulting data into appropriate responses. I try very hard not to add business logic in this layer, keeping it like a clean HTTP façade.

In this application, it was implemented UserController, CategoryController and TimeEntryController classes to group each resources operations. There will be also an AuthController to handle authorization and token generation.

I think it is a good practice to create specific Request and Response serializable DTOs (Data Transfer Objects) for each endpoint group, so it is not necessary to transfer unused fields to clients, and it also allows to segregate json-serialization and validation annotations, leaving our model as clean as possible.

For instance, it is not necessary to return ‘password' data in responses, as well the internal ‘id’ usually doesn't make sense in requests bodies.

Of course, in this simple implementation, all of this is clearly a huge over-engineering, but I implemented that intentionally to show my point of view.

So I created a request and a response DTO with specific annotated fields for each model class. To automatically map the request/response classes to model classes, and after to entity classes, I like to use de mapstruct library and implement the required interfaces.

The identified endpoints of each resource were as follows:

-- UserController:PostMapping
public ResponseEntity<UserResponseDTO> createUser(@Valid @RequestBody UserRequestDTO userReqDTO)
@PutMapping("{id}")
public ResponseEntity<UserResponseDTO> updateUser(@NotNull @PathVariable("id") Long id, @Valid @RequestBody UserRequestDTO userReqDTO)
@DeleteMapping("{id}")
public ResponseEntity removeUser(@NotNull @PathVariable("id") Long id)

@GetMapping
public ResponseEntity<List<UserResponseDTO>> getAllUsers()
@GetMapping("{id}")
public ResponseEntity<UserResponseDTO> getUserById(@NotNull @PathVariable("id") Long id)

-- CategoryController
:
@PostMapping()
public ResponseEntity<CategoryResponseDTO> createCategory(@PathVariable("userId") Long userId, @Valid @RequestBody CategoryRequestDTO categoryReqDTO)

@PutMapping("{categoryId}")
public ResponseEntity<CategoryResponseDTO> updateCategory(@PathVariable("userId") Long userId, @PathVariable("categoryId") Long categoryId, @Valid @RequestBody CategoryRequestDTO categoryReqDTO)
@DeleteMapping("{categoryId}")
public ResponseEntity removeCategory(@PathVariable("userId") Long userId, @PathVariable("categoryId") Long categoryId)
@GetMapping()
public ResponseEntity<List<CategoryResponseDTO>> getCategoriesByUser(@PathVariable("userId") Long userId)
@GetMapping("{categoryId}")
public ResponseEntity<CategoryResponseDTO> getCategoriesById(@PathVariable("userId") Long userId, @PathVariable("categoryId") Long categoryId)

-- TimeEntryController
:
@PostMapping
public ResponseEntity<?> createTimeEntry(@PathVariable("userId") Long userId, @Valid @RequestBody TimeEntryRequestDTO timeEntryReqDTO)

@PutMapping("{timeId}")
public ResponseEntity<?> updateTimeEntry(@PathVariable("userId") Long userId, @PathVariable("timeId") Long timeId, @RequestBody TimeEntryRequestDTO timeEntryReqDTO)
@DeleteMapping("{timeId}")
public ResponseEntity removeTimeEntry(@PathVariable("userId") Long userId, @PathVariable("timeId") Long timeId)
@GetMapping()
public ResponseEntity<List<TimeEntryResponseDTO>> getTimeEntriesByUsers(@PathVariable("userId") Long userId)
@GetMapping("{timeId}")
public ResponseEntity<TimeEntryResponseDTO> getTimeEntryById(@PathVariable("userId") Long userId, @PathVariable("timeId") Long timeId)
-- AuthController:@PostMapping("auth")
public ResponseEntity<TokenDTO> authorize(@Valid @RequestBody UserAuth userAuth)

For each class/endpoint, it was implemented unit tests with Spring-Test and MockMvc.

Creating Service classes

The service layer/package groups classes responsible for business logic implementation, providing operation results to the controller layer and accessing the repository layer if necessary.

So, basically it's implemented some CRUD operations applying the business logic for each one.

It also was implemented unit tests with Junit 5 and Mockito.

Creating Repository classes

The repository layer/package groups classes that persist and retrieve persistent data. We could use DAO classes here as well, but in this project it was used the Spring Data interfaces and JPA Entity classes for each resource.

For unit tests, it was used Junit 5 and DataJpaTest.

Security Configuration

After ensuring the resources endpoints were working, it was time to enable security configuration, so all endpoints would be protected except de authentication endpoint to provide the token to use the API.

The security configuration was made implementing a class named SecurityConfiguration that extends WebSecurityConfigurerAdapter. To encrypt the password it was used the bcrypt encoder, which is very popular nowadays. The authentication filter (TokenAuthFilter) was also configured in this class, and the application was configured to be Stateless.

@EnableWebSecurity
@Configuration
public class
SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private UserService userService;
@Autowired
private TokenService tokenService;


@Override // authentication configs
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}

@Override // Authorization configs
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/timetracker/auth").permitAll()
.anyRequest().authenticated()
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilterBefore(new TokenAuthFilter(tokenService), UsernamePasswordAuthenticationFilter.class);
}

@Override // static resources (css, js, images, ...)
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

@Bean
@Override // creates authentication manager bean to be injected in auth controller
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}

The implemented Authentication Filter is responsible to decode the JWT token for each request and set the current request as authenticated if the token is valid.

public class TokenAuthFilter extends OncePerRequestFilter {

private final TokenService tokenService;

public TokenAuthFilter(TokenService tokenService) {
this.tokenService = tokenService;
}


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || authHeader.isEmpty() || authHeader.length() <= 7) {
chain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7, authHeader.length());
boolean isValid = tokenService.isTokenValid(jwt);
if (isValid) {
Claims claims = tokenService.extractClaims(jwt);
Role role = Role.valueOf(claims.getAudience());
UserAuth userAuth = new UserAuth(claims.getSubject(), null, role);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userAuth, null, userAuth.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}

}

The authorization rules was encapsulated in the AuthorizationRules class, which can be used in the resources controllers methods.

Error handlers

In the error package it was created custom unchecked exceptions to be used in services classes, that was mapped in RestExceptionHandler class.

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Object> handleResourceNotFound(ResourceNotFoundException ex) {
ErroDetails error = new ErroDetails(ex.getClass().getSimpleName(), ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(ResourceAlreadyRegisteredException.class)
public ResponseEntity<Object> handleResourceAlreadyRegistered(ResourceAlreadyRegisteredException ex) {
ErroDetails error = new ErroDetails(ex.getClass().getSimpleName(), ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(TimeTrackerOperationException.class)
public ResponseEntity<Object> handleTimeTrackerOperation(TimeTrackerOperationException ex) {
ErroDetails error = new ErroDetails(ex.getClass().getSimpleName(), ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<Object> handleAuthorization(AuthorizationException ex) {
ErroDetails error = new ErroDetails("AuthorizationException", "Operation not allowed for authenticated user");
return new ResponseEntity<>(error, HttpStatus.FORBIDDEN);
}

@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
Map<String, String> fieldErrors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
fieldErrors.put(fieldName, errorMessage);
});
ErroDetails errorDetails = new ErroDetails("ValidationException", "Invalid fields", fieldErrors);
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}

@Override
public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ErroDetails error = new ErroDetails("JsonParseException", "Could not parse Json");
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

@Override // catch any other exception for standard error message handling
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
ErroDetails error = new ErroDetails("InternalErrorException", ex.getMessage());
return new ResponseEntity<>(error, headers, status);
}
}

Functional Tests

For functional tests, it was implemented a new segregated project using cucumber and rest-assured to test the endpoints (black box tests).

The cucumber features was placed int the resources directory, whereas the tests steps was implemented for each resource class.

As the next step, it could be added the swagger configuration for API documentation.

--

--