Skip to main content

Building a Restful Webservice using Spring

·1727 words·9 mins
Table of Contents
Spring - This article is part of a series.
Part : This Article

Building a Restful Webservice using SpringBoot
#

The first thing to do would be to head over to https://start.spring.io to download the starter project. We would pick the following dependencies for our project. It would be a simple web service, which I will explain as I go along. Here is a sample starter project link you can use to follow along.

Spring initializer Sample

Dependencies we pick
#

  • Web (For web services)
  • Devtools (Fast development flow)
  • JPA (For our DB Operations)
  • H2 (For fast in-memory DB)

Once downloaded, we will extract and import the project to Eclipse or any IDE of your choice.

Simple Hello World API
#

We will just create another file in the base package to create a Hello World response. It will be a GET API giving the response Hello World. To do that, we will use the below code.

package me.vigneshm.rest.webservices.restfulwebservices;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloControlller {
    
    @GetMapping("/hello")
    public String helloWorld() {
        return "Hello World";
    }
}

We can start the application and access the URL http://localhost:8080/hello to get a Hello World Response.

We will enhance it to return a Bean. Let’s first create the bean.

package me.vigneshm.rest.webservices.restfulwebservices;

public class HelloWorldBean {
    
    private String message;
    
    public HelloWorldBean(String message) {
        super();
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "HelloWorldBean [message=" + message + "]";
    }
}

Now for the controller code.

@GetMapping("/hello-bean")
public HelloWorldBean helloWorldBean() {
    return new HelloWorldBean("Hello");
}

We can access the API at http://localhost:8080/hello-bean. We will get a JSON response.

{"message":"Hello"}

We will now try adding a path variable.

@GetMapping("/hello-bean/path-variable/{name}")
public HelloWorldBean helloWorldBeanPathVariable(@PathVariable String name) {
    return new HelloWorldBean(String.format("Hello, %s", name));
}

@PathVariable is the annotation that is key for this mapping. This API can be accessed by http://localhost:8080/hello-bean/path-variable/Sir. It will give below JSON response.

{"message":"Hello, Sir"}

User API
#

We will start with creating a Bean to represent the user.

package me.vigneshm.rest.webservices.restfulwebservices.user;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Date birthDate;
}

Now, I am not going to get into JPA just yet, let’s just create a dummy mock-up service to implement the JPA operations.

package me.vigneshm.rest.webservices.restfulwebservices.user;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.springframework.stereotype.Component;

@Component
public class UserDaoService {
    private static List<User> users = new ArrayList<>();
    private static int usersCount = 3;
    static {
         users.add(new User(1, "Adam", new Date()));
         users.add(new User(2, "Eve", new Date()));
         users.add(new User(3, "Bob", new Date()));
    }
    
    public List<User> findAll(){
        return users;
    }
    
    public User save(User user) {
        if(user.getId() == null ) {
            ++usersCount;
            user.setId(usersCount);
        }
        users.add(user);
        return user;
    }
    
    public User findOne(int id) {
        for (User user: users) {
            if (user.getId() == id)
                return user;
        }
        return null; 
    }
}

Now, in the same way, we create the Hello World controller, let’s go ahead and create the User Controller.

package me.vigneshm.rest.webservices.restfulwebservices.user;

import java.net.URI;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

@RestController
public class UserResource {

    @Autowired
    public UserDaoService userDaoService;
    @GetMapping("/users")
    public List<User> retriveUsers(){
        return userDaoService.findAll();
    }
    
    @GetMapping("/users/{id}")
    public User retriveUserById(@PathVariable int id){
        User user = userDaoService.findOne(id);
        if (user == null)
            throw new UserNotFoundException("id-"+id);
        return user;
    }
    
    @PostMapping("/user")
    public ResponseEntity createUser(@RequestBody User user) {
        User savedUser = userDaoService.save(user);
        URI createdLocation = ServletUriComponentsBuilder
        .fromCurrentRequest()
        .path("{id}")
        .buildAndExpand(savedUser.getId())
        .toUri() ;
        return ResponseEntity.created(createdLocation).build();
    }
}

In the above example, we have a user get the mapping to get all users, user by id, and add a user. When we add a user, we use ResponseEntity and follow the HTTP response status using 201. We also pass the path to show created location in the header(http://localhost:8080/user4)

For users not found, we got an exception with UserNotFoundException. This is to maintain HTTP standards properly to return 404 when the user is not found.

package me.vigneshm.rest.webservices.restfulwebservices.user;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {
        super(message);
    }   
}

We will now try adding a Generic Exception handler for all resources. First, we will define the common error message object.

package me.vigneshm.rest.webservices.restfulwebservices.user.exception;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ExceptionResponse {
    private String errorCode;
    private String errorMessage;    
}

Now, we will add the exception handler using the object.

package me.vigneshm.rest.webservices.restfulwebservices.user.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionalHandler 
extends ResponseEntityExceptionHandler{
    @ExceptionHandler(Exception.class)
    public final ResponseEntity<Object> handleAllException(Exception ex,
            WebRequest request) throws Exception {
        ExceptionResponse exceptionResponse = new ExceptionResponse("ERR001", ex.getMessage());
        return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    @ExceptionHandler(UserNotFoundException.class)
    public final ResponseEntity<Object> handleUserNotFoundException(Exception ex,
            WebRequest request) throws Exception {
        ExceptionResponse exceptionResponse = new ExceptionResponse("ERR002", ex.getMessage());
        return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
    }
}

@RestController to describe that it’s going to return API responses, @ControllerAdvice to intercept all controllers in the project. We extent the ResponseEntityExceptionHandler to provide the required functionality. We add @ExceptionHandler to the methods to handle the exceptions. We define different HttpStatus codes and can also define different error codes as well.

Now, let’s see deleting a resource. The code is simple and straightforward, first, we will see code added in the Dao service.

public User deleteById(int id) {
    Iterator<User> iterator = users.iterator();
    while (iterator.hasNext()) {
        User user = iterator.next();
        if (user.getId() == id) {
            iterator.remove();
            return user;
        }
    }
    return null;
}

Next, code for UserResource/ UserController.

@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable int id) {
    User user = userDaoService.deleteById(id);
    if (user == null) {
        throw new UserNotFoundException("id-"+id);
    }
}

That’s it, this will return 200 for successful deletion and 404 for a user not found.

Let’s add the validations for the fields we have defined. First, we add the dependency for it.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation </artifactId>
</dependency>

Now for the annotations. We will add validations in the Bean definition.

public class User {
    private Integer id;
    @Size(min = 2, message = "Name should be greater than 2 characters long")
    private String name;
    @Past(message="Birth date should be a past date")
    private Date birthDate;
}

We will add @Valid annotation in the method signature.

public ResponseEntity createUser(@Valid @RequestBody User user) {

Now, data will be validated, but we will always get 400 bad requests, rather than getting some proper response. We will add a response error handler for this now.

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {

    ExceptionResponse exceptionResponse = new ExceptionResponse("ERR003", ex.getMessage());
    return new ResponseEntity(exceptionResponse, HttpStatus.BAD_REQUEST);
}

Now, we will add HATEOS(Hypermedia as the Engine of Application State). First, we will add the dependency.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

We will modify our existing GET user to API to return a link to get all users as well.

@GetMapping("/users/{id}")
public EntityModel<User> retriveUserById(@PathVariable int id){
    User user = userDaoService.findOne(id);
    if (user == null)
        throw new UserNotFoundException("id-"+id);
    
    EntityModel<User> userModel = EntityModel.of(user);
    WebMvcLinkBuilder linkToUsers = 
            linkTo(methodOn(this.getClass())
            .retriveUsers());
    userModel.add(linkToUsers.withRel("all-users"));
    
    return user model;
}

If you are unaware of what HATEOAS is, please check out the Wikipedia article.

Internationalization (i18n) to support users from different countries. For this, we will go with the hello world method. We first need to define messages.properties file in the resources folder.

messages.properties - English(Default) messages_fr.properties - French

Next, we will get the input from the header parameter (Accept-Language).

@Autowired
MessageSource messageSource;

@GetMapping("/hello-international")
public String helloWorldInternational(
        @RequestHeader(name="Accept-Language", required=false) Locale locale) {
    
    return messageSource.getMessage("hello", null, "Default Message",locale);
}

We can get the Locale directly and not from the method signature as well.

return messageSource.getMessage(
        "hello", null, "Default Message",
        LocaleContextHolder.getLocale());

Now, in the API header, pass Accept-Language as fr, to get a response of Bonjour.

Let’s add support for XML now. All that is needed is to add a dependency and send a header.

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

Now in the API, just pass the header parameter, accept with value application.xml. That’s it we will get a response in XML format.

Let’s add open api documentation. The depedency for that is below.

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.4</version>
</dependency>

Access the documentation at http://localhost:8080/swagger-ui/index.html.

Now, we will use Spring Actuator. This gives us tools to monitor the application, enables a lot of production-ready features. First, here’s the dependency.

<dependency >
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator </artifactId>
</dependency>

By default, only a few endpoints will be accessible. We will add the below-line application.properties to enable all management endpoints.

management.endpoints.web.exposure.include=*

Now, access the URLs via http://localhost:8080/actuator.

Let’s add HAL Explorer support to expose our APIs. This is just a good to have a feature to easily play with APIs that already support HATEOAS. The dependency is given below.

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-rest-hal-explorer </artifactId>
</dependency>

We can access the URL at http://localhost:8080/.

Let’s look at filtering. This would be to stop sending a few resources in the response. This can be done in 2 ways. Static and Dynamic. We will look at static first

Static Filtering is done by adding @JsonIgnore to the field in Bean Definition or by adding @JsonIgnoreProperties to the class.

@JsonIgnore
private String field3;
@JsonIgnoreProperties(value = {"field2", "field3"})
public class SomeBean {

Another way would be to do dynamic filtering. To send fields based on some conditions. First, we add the Filter to the Bean.

@JsonFilter("SomeBeanFilter")
public class SomeBean {

Next, we add the code to the controller.

@GetMapping("/filtering")
public MappingJacksonValue getSomeBean() {
    SomeBean someBean = new SomeBean("field1", "field2", "field3");
    SimpleBeanPropertyFilter simpleBeanPropertyFilter = 
            SimpleBeanPropertyFilter
            .filterOutAllExcept("field1", "field2");
    FilterProvider filterProvider = new SimpleFilterProvider()
            .addFilter("SomeBeanFilter", simpleBeanPropertyFilter);
    MappingJacksonValue mapping = new MappingJacksonValue(someBean); 
    mapping.setFilters(filterProvider); 
    return mapping;
}

Now, let’s look at versioning. A simple example we can see here.

Basic approach with URI.

@GetMapping("v1/person")
public PersonV1 getPersonV1() {
    return new PersonV1("Bobby Fischer");
}
@GetMapping("v2/person")
public PersonV2 getPersonV2() {
    return new PersonV2(new Name("Magnus", "Carlsen"));
}

With Request Parameters

@GetMapping(value = "/person-rqparam", params="version=1")
public PersonV1 getPersonV1Param() {
    return new PersonV1("Bobby Fischer");
}

Can be called at http://localhost:8080/person-rqparam?version=2

With header params

@GetMapping(value = "/person/header", headers="X-API-VERSION=1")
public PersonV1 getPersonV1Header() {
    return new PersonV1("Bobby Fischer");
}

This can be accessed with the below URL and header.

http://localhost:8080/person/header Header: X-API-VERSION 1

Similarly, we can achieve it by content negotiation as well. There is no one good approach for Versioning in APIs. The different URIs or request params result in URI pollution but are easier to cache and access via browsers. But, using headers is better when URI is not to be changed but involves complexity in implementation from the client-side.

We have seen pretty much everything required about REST APIs with Spring in the is exhaustive post. We will explore JPA in a future post, followed by microservices as well.

Spring - This article is part of a series.
Part : This Article