REST¶
Starter spring-boot-starter-web
allows you to easily and quickly create a web application. It provides autoconfiguration for this layer and allows you to use objects that will help in applying the MVC (Model-View-Controller) architectural pattern.
The MVC pattern assumes that the entire application is divided into three separate layers:
model
- represents objects that describe the problem to be solvedview
- represents the layer that is responsible for displaying a part of the model based on the user's actioncontroller
- a layer that accepts data and actions from the user and decides what view to display (and update the model based on it)
Spring Web
, in addition to controllers that display the view using prepared HTML files, also allows you to create REST services (the so-called REST API). In other words, this launchpad allows you to create an application:
- in which views (and thus the frontend layer) are embedded (MVC)
- the results of which can be consumed by other applications (e.g. using Angular)
The application using spring-boot-starter-web
starts the application by default using Tomcat and port 8080.
DispatcherServlet¶
Before we move on to showing how to define a controller, you need to understand what role does the so-called DispatcherServlet
play. DispatcherServlet is an object that we do not deal with directly, but in the context of a Spring-based web application it has a very important task. This is called A front controller
that accepts all incoming HTTP requests. Then, based on data such as:
- the HTTP method is used when making an HTTP request (e.g. POST, PUT, GET, DELETE)
- URL where the request was sent
- request parameters
- the type of object that comes in the body (body) of the HTTP request
filters the endpoints available in the application and selects the one that can handle such a request (ie delegates work).
Creation of REST services¶
In order to create a controller that is visible both in context and also visible to DispatcherServlet
, you need to create a class that is annotated @Controller
. Thanks to this, the bean created in this way will go to this part of the context about which it "knows" DispatcherServlet
- WebApplicationContext
.
Each controller can consist of many endpoints that we create by defining public methods inside this class which are marked with the annotation @RequestMapping
. This annotation can be set using the field:
path
- path to the endpointmethod
- HTTP method handled by endpoint
ResponseEntity¶
REST services, based on some HTTP request, have to respond with an object consisting of status, headers and body. This object is represented by a generic type ResponseEntity
, which specifies the type of object that is represented in the response body. We can create the ResponseEntity
instance using the new
operator or using builder
, e.g .:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String text;
}
// creating a ResponseEntity with a constructor
new ResponseEntity<>(HttpStatus.NO_CONTENT); // ResponseEntity with the status 204
new ResponseEntity<>("Response Body", HttpStatus.CREATED); // response entity with body and status 201
new ResponseEntity<>("ResponseBody",
new LinkedMultiValueMap<>(Map.of("some-header", List.of("header-value"))),
HttpStatus.OK); // response entity with body, headers and status 200
// creating a ResponseEntity with a constructor
ResponseEntity.ok();
ResponseEntity.badRequest().body("Something went bad");
ResponseEntity.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.body(new Message("helloMsgAsJson"));
The example below shows how to create a simple controller with two endpoints. One of them is available at the path / api/hello
and returns a Message
object with a fixed value (JSON by default). The second is available under the /api/hi
URI. It returns a String
with the texthi
:
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class HelloWorldController {
@RequestMapping(method = RequestMethod.GET, path = "/api/hello")
public ResponseEntity<Message> sayHello() {
return ResponseEntity.ok().body(new Message("Hello World"));
}
@RequestMapping(method = RequestMethod.GET, path = "/api/hi")
public ResponseEntity<String> sayHi() {
return ResponseEntity.ok().body("hi");
}
}
As both of the above endpoints require the GET
operation, we can check their result in the browser by typing in the address bar:
http://localhost:8080/api/hello
http://localhost:8080/api/hi
NOTE: The port the application is running on may be different.
NOTE: When testing REST services, it is better to use tools such as Postman instead of the browser, especially since the browser will not allow you to easily send requests using the
PUT
orDELETE
.
@ResponseBody annotation¶
When creating endpoints, we rarely set additional response headers at their level. Usually, we perform some operation and return nothing in the response body (and we use the HTTP 204 status) or we return the created or downloaded object (and we usually return the 200 or 201 status). Therefore, instead of always returning a ResponseEntity
object, it would be better to only return the object that should be wrapped in the response body (e.g. as a JSON object) or nothing (void
) in case we want to return an empty body.
This possibility is offered by the use of the @ResponseBody
annotation. Using it over the endpoint definition saves us from having to return the ResponseEntity
object. Instead, from the method representing the endpoint, we return the object that is to go to the response body, e.g .:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloWorldController {
@ResponseBody
@RequestMapping(method = RequestMethod.GET, path = "/api/hello")
public Message sayHello() {
return new Message("Hello World");
}
@ResponseBody
@RequestMapping(method = RequestMethod.GET, path = "/api/hi")
public String sayHi() {
return "hi";
}
@ResponseBody
@RequestMapping(method = RequestMethod.GET, path = "/api/nothing")
public void doNothing() {
}
}
The @ResponseBody
annotation looks for a suitable converter based on the HTTPAccept
header. By default, the response will be converted to JSON.
Annotation @PathVariable¶
REST services often allow you to download a collection of certain objects or an object with a specific identifier. This identifier is most often part of the URL used when sending the request. It is a variable part of the path. We define such a part:
- by wrapping part of it (and any name of your choice) inside
{
and}
characters, e.g.{id}
- by mapping the variable part of the path to the method argument with the annotation
@PathVariable
, in which we indicate the name used in the path
The next example shows how to map a variable part of a path to a variable:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloWorldController {
@ResponseBody
@RequestMapping(method = RequestMethod.GET, path = "/api/hello/{id}")
public Message sayHelloWithId(@PathVariable(name = "id") final Integer id) {
return new Message("Hello World " + id);
}
}
By sending the following request:
curl --location --request GET 'http://localhost:8080/api/hello/1'
in response we get the following body:
{
"text": "Hello World 1"
}
When using the @PathVariable
annotation, remember that:
- the variable type can be adjusted to the needs (we can use e.g.
Integer
,Long
,String
orUUID
- the conversion will take place automatically) - the
name
field is optional when the name used in the path is equal to the name of the method argument (i.e. in the previous example, we could remove this field, because both the method argument and the variable part of the path are namedid
)
Annotation @RequestBody¶
Some endpoints may expect some object in the request sent. This object is often sent as JSON or XML. Similar to using the @ResponseBody
annotation, we can use one of the configured converters, to get an object created in Java. For this purpose, we use the @RequestBody
annotation next to the method argument that represents the request body.
The following example shows the use of this annotation, and also shows that it can be used in tandem with [@PathVariable
] (# pathvariable-annotation
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@Controller
public class MessageController {
private final List<Message> messages = new ArrayList<>();
@ResponseBody
@RequestMapping(value = "/api/messages", method = RequestMethod.POST)
public void createMessage(@RequestBody final Message message) {
messages.add(message);
}
@ResponseBody
@RequestMapping(value = "/api/messages/{index}", method = RequestMethod.PUT)
public void updateMessage(@RequestBody final Message message, @PathVariable final Integer index) {
messages.get(index).setText(message.getText());
}
}
The above controller can be tested by sending the following requests:
curl --location --request POST 'http://localhost:8080/api/messages' \
--header 'Content-Type: application/json' \
--data-raw '{
"text": "hello"
}'
curl --location --request PUT 'http://localhost:8080/api/messages/1' \
--header 'Content-Type: application/json' \
--data-raw '{
"text": "new-hello"
}'
Set up response statuses¶
When creating a certain endpoint, the default status is 200 (or 500 in case of exception). While performing some operations, we want this status to have a different value, e.g.:
201
(CREATED) when we have successfully created the object204
(NO_CONTENT) when the action was successfully performed but no body was returned
We can set the response status in two ways:
- by setting it to the
ResponseEntity
build level - by setting it using the
@ResponseStatus
annotation (over the method representing the endpoint)
The next example shows the use of the @ResponseStatus
annotation:
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@Controller
public class MessageController {
private final List<Message> messages = new ArrayList<>();
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
@RequestMapping(value = "/api/messages", method = RequestMethod.POST)
public void createMessage(@RequestBody final Message message) {
messages.add(message);
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
@RequestMapping(value = "/api/messages/{index}", method = RequestMethod.PUT)
public void updateMessage(@RequestBody final Message message, @PathVariable final Integer index) {
messages.get(index).setText(message.getText());
}
}
NOTE: Using the
@ResponseStatus
annotation, it is better to return the object that should be wrapped in the response body from the method representing the endpoint than theResponseEntity
object.
Annotation @RequestParam¶
Requests can also have required (or optional) parameters. These parameters are at the end of the URL, separated by the &
character, and the key and value are separated by the =
character, e.g .: http://localhost:8080/api/resources?param1=val1¶m2=val2
In order to retrieve the values of these parameters into the method arguments, we use the @RequestParam
annotation. Like the @PathVariable
annotation, it allows you to define the name of a parameter, whether it is required, and it places responsibility for matching the value to the type to the programmer.
The example shows the use of the @RequestParam
annotation, from which a response can be obtained via the following URL:
http://localhost:8080/api/messages/search?param1=7¶m2=eff3168f-974d-4a37-888f-f96a08658525
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@Controller
public class MessageController {
@RequestMapping(method = RequestMethod.GET, path = "/api/messages/search")
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public void searchMessages(@RequestParam(name = "param1") final Integer param1, @RequestParam(name = "param2", required = false) final UUID param2) {
// code responsible for search
}
}
Shortened notations¶
All of the above examples were created using basic annotations. However, Spring offers to use some class-level operations instead of a method. Some of the annotations, in turn, can be used on the class and methods simultaneously.
Instead of using the @ResponseBody
annotation on all methods representing endpoints, it can only be used over the class, i.e. instead of defining endpoints as follows:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloWorldController {
@ResponseBody
@RequestMapping(method = RequestMethod.GET, path = "/api/hello")
public Message sayHello() {
return new Message("Hello World");
}
@ResponseBody
@RequestMapping(method = RequestMethod.GET, path = "/api/hi")
public String sayHi() {
return "hi";
}
}
we can define them as below:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@ResponseBody
public class HelloWorldController {
@RequestMapping(method = RequestMethod.GET, path = "/api/hello")
public Message sayHello() {
return new Message("Hello World");
}
@RequestMapping(method = RequestMethod.GET, path = "/api/hi")
public String sayHi() {
return "hi";
}
}
Moreover, REST services very often use a combination of the @Controller
annotation together with@ResponseBody
over the class definition. In this case, it is better to use @RestController
, which is a combination of the two, e.g .:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@RequestMapping(method = RequestMethod.GET, path = "/api/hello")
public Message sayHello() {
return new Message("Hello World");
}
@RequestMapping(method = RequestMethod.GET, path = "/api/hi")
public String sayHi() {
return "hi";
}
}
The @RequestMapping
annotation allows you to define an endpoint available under many HTTP methods. Most often, however, the value of the method
field in the@RequestMapping
annotation has a single value. In such a case, it is worth replacing its use with one of the annotations where this field does not need to be entered, and whose value results from the name of the annotation, i.e .:
@GetMapping
@PostMapping
@PutMapping
@PatchMapping
@DeleteMapping
Hence the following annotation:
@RequestMapping(method = RequestMethod.GET, path = "/api/hello")
można zastąpić:
@GetMapping("/api/hello")
or similarly the following annotations:
@RequestMapping(value = "/api/messages", method = RequestMethod.POST)
@RequestMapping(value = "/api/messages/{index}", method = RequestMethod.PUT)
@RequestMapping(value = "/api/messages/{index}", method = RequestMethod.DELETE)
can be simplified accordingly:
@PostMapping("/api/messages")
@PutMapping("/api/messages/{index}")
@DeleteMapping("/api/messages/{index}")
Furthermore, if multiple endpoints in a class are available under a path that has a common origin, they can be defined directly above the class (using the @RequestMapping
annotation), and a unique part of the endpoint path directly above the method (also using the@RequestMapping
annotation) ), e.g .:
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(path = "/api")
public class HelloWorldController {
@GetMapping("/hello") // endpoint available under /api/hello
public Message sayHello() {
return new Message("Hello World");
}
@GetMapping("/hi") // endpoint available under /api/hi
public String sayHi() {
return "hi";
}
}