Computer scienceBackendSpring BootWebREST

Customizing REST responses

11 minutes read

In the world of web development, especially when working with RESTful APIs, we often encounter situations where the response from a controller method needs to be customized based on certain conditions. For instance, you might want to send different responses depending on whether a resource was requested in a specific format or whether the requested resource is found.

Mastering the effective management of these scenarios is pivotal in constructing resilient and adaptable web services. This topic will help you gain a deeper understanding of how to control and customize HTTP responses in your Spring Boot applications.

HTTP responses

As you may already know, an HTTP response consists of three main parts:

  • Status code. This is a three-digit number sent by the server to indicate the result of the requested resource. For example, 200 means OK, 404 means Not Found, 500 means Internal Server Error, and so on. Here is a list of available status codes and their meanings.

  • Headers provide additional information about the response or the request. One important header is Content-Type, which specifies the media type of the resource. For example, Content-Type: application/json indicates that the server is returning JSON data.

  • Body (optional). This is the actual content of the response. It can be data in the form of text or other formats like JSON, XML, HTML, and others.

In Spring Boot, the return type of a method of a @RestController annotated class typically determines the response body. For example, if a method returns a String, the response body will contain that string. You can use the @ResponseStatus annotation to specify the status code. The Content-Type is usually inferred from the data type of the response body, but you can also set it explicitly using the produces attribute in the handler method annotations.

Java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping(path = "/hello", produces = "text/plain")
    @ResponseStatus(HttpStatus.OK)
    public String hello() {
        return "Hello World";
    }
}
Kotlin
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
class DemoController {

    @GetMapping(path = ["/hello"], produces = ["text/plain"])
    @ResponseStatus(HttpStatus.OK)
    fun hello(): String = "Hello World"
}

In this example, the hello() method always returns a 200 OK response with Content-Type: text/plain and a body of Hello World.

But what if you need to return different responses based on certain conditions? For instance, suppose you have a method that can return an image in either PNG or JPEG format. Depending on the client's request, it might be necessary to return a 200 OK response with Content-Type: image/png or Content-Type: image/jpeg. Or, if the requested image is not found, you might want to return a 404 Not Found response.

We will explore the available options in upcoming sections.

HttpServletResponse

Let's first see how we can customize our HTTP response using the HttpServletResponse object. This object allows us to set the status, headers, and body of our response. Let's modify the hello method to use HttpServletResponse:

Java
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
public class DemoController {

    @GetMapping(path = "/hello")
    public void hello(HttpServletResponse response) throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("text/plain");
        response.getWriter().write("Hello World");
    }
}
Kotlin
import jakarta.servlet.http.HttpServletResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.io.IOException

@RestController
class DemoController {

    @GetMapping(path = ["/hello"])
    @Throws(IOException::class)
    fun hello(response: HttpServletResponse) {
        response.status = HttpServletResponse.SC_OK
        response.contentType = "text/plain"
        response.writer.write("Hello World")
    }
}

In this example, we're injecting HttpServletResponse and using its methods to set the status code, content type, and response body.

While this approach gives us more control over the response, it's not without drawbacks. HttpServletResponse is a lower-level Servlet API. Using it directly in your controllers makes code more error-prone and less readable. It also ties your controller to the Servlet API, reducing its portability. Moreover, with this approach, since the response is fully prepared within the method, there is no need to return anything, resulting in a void return type. This deviates from the standard practice where controller methods return a response body.

These drawbacks can make this approach less than ideal for many applications. Thankfully, Spring provides a more elegant and flexible way to customize HTTP responses with the help of the ResponseEntity class. In the next section, we'll take a closer look at how to use it.

ResponseEntity

This class encapsulates all the components of an HTTP response, and its methods allow us to set these components in a more intuitive and readable way.

One of the key features of ResponseEntity is its builder methods, which provide a fluent API to create an instance of ResponseEntity. Here are some of the commonly used builder methods:

  • status(HttpStatusCode): sets the status code of the response, such as HttpStatus.BAD_REQUEST.

  • header(String, String): adds a single header value under the given name.

  • headers(HttpHeaders): sets all headers with the given HttpHeaders instance.

  • body(T): sets the body of the response.

  • notFound(): sets the 404 Not Found status code.

Usually, you first call a method to set a status code and then chain other builder methods to set other response components.

Let's see how you can use ResponseEntity and its builder to customize a handler method. In this example, the method will find and return a BufferedImage by its name in either PNG or JPEG format depending on the request parameter. If the requested image is not found, it will return a 404 Not Found response.

Java
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Map;

@RestController
public class DemoController {
    private static final Map<String, BufferedImage> images = Map.of(
            "green", createImage(Color.GREEN),
            "magenta", createImage(Color.MAGENTA)
    );

    @GetMapping(path = "/image")
    public ResponseEntity<BufferedImage> getImage(@RequestParam String name,
                                                  @RequestParam String mediaType) {
        BufferedImage image = images.get(name);
        if (image == null) {
            return ResponseEntity.notFound().build();
        }

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(mediaType))
                .body(image);
    }

    private static BufferedImage createImage(Color color) {
        BufferedImage image = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        g.setColor(color);
        g.fillRect(0, 0, 20, 20);
        g.dispose();

        return image;
    }
}
Kotlin
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.awt.Color
import java.awt.image.BufferedImage

@RestController
class DemoController {
    private val images: Map<String, BufferedImage> = mapOf(
        "green" to createImage(Color.GREEN),
        "magenta" to createImage(Color.MAGENTA)
    )

    @GetMapping(path = ["/image"])
    fun getImage(
        @RequestParam name: String,
        @RequestParam mediaType: String
    ): ResponseEntity<BufferedImage> {
        val image = images[name]
            ?: return ResponseEntity.notFound().build()

        return ResponseEntity
            .ok()
            .contentType(MediaType.parseMediaType(mediaType))
            .body(image)
    }

    private fun createImage(color: Color): BufferedImage {
        val image = BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB)
        val graphics = image.createGraphics()
        graphics.color = color
        graphics.fillRect(0, 0, 20, 20)
        graphics.dispose()

        return image
     }
}

In this example, a few BufferedImage objects are created and stored in a Map. The getImage method accepts image name and media type as request parameters. ResponseEntity<BufferedImage> is used as the return type of the method. If the image is found, we set it as the body of the response. The Content-Type header is set to either image/png or image/jpeg depending on the mediaType parameter. If the image is not found, we return a ResponseEntity with a status of 404 Not Found but without a body.

In its default configuration, a Spring Boot controller is unable to convert a BufferedImage into an array of bytes in PNG or JPEG format. To facilitate this conversion, we need to register the appropriate HttpMessageConverter<T> bean within one of our configuration files. This helps Spring understand how to transform the BufferedImage into the desired format:

Java
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public HttpMessageConverter<BufferedImage> bufferedImageHttpMessageConverter() {
        return new BufferedImageHttpMessageConverter();
    }
}
Kotlin
@SpringBootApplication
class Application {
    @Bean
    fun bufferedImageHttpMessageConverter(): HttpMessageConverter<BufferedImage> =
        BufferedImageHttpMessageConverter()
}

While there's a broad spectrum of valid media types available, Spring's default configuration only supports a limited subset. If you need to work with a media type outside of this subset, you'll need to create custom converters or use additional libraries in your project.

Upon launching the application and navigating to the following URL in a web browser

http://localhost:8080/image?name=green&mediaType=image/jpeg

you will be presented with a green square image, rendered in JPEG format. If you alter your request to

http://localhost:8080/image?name=magenta&mediaType=image/png

you'll receive a magenta square. This time, it is displayed in PNG format.

This example demonstrates the power and flexibility of ResponseEntity. With it, you can easily construct and return different responses based on various conditions.

Conclusion

In this topic, we explored different ways to customize HTTP responses in Spring Boot. The first method is to use annotations to define the content type and response code. This method is convenient and concise but lacks flexibility. Another method is to use HttpServletResponse. This approach offers a high degree of control but has some drawbacks. It is low-level and deviates from the standard pattern of returning a response body from controller methods, which can lead to less intuitive code.

Finally, we introduced ResponseEntity, a class provided by Spring that encapsulates an entire HTTP response, including headers, body, and status. We discussed its builder methods and demonstrated how to use them to create a ResponseEntity with a customized response.

68 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo