Spring Boot - Rest Controller

Spring RestController Usage

Rest Controller Annotations

Annotations used for Spring MVC are used in Spring Boot too. Here are the common annotations:

  • @RestController - @Controller + @ResponseBody
  • @ResponseBody - indicates that the result type should be written straight in the response body in whatever format you specify like JSON or XML.
  • @RequestMapping - This annotation is used at both the class and method level. The @RequestMapping annotation is used to map web requests onto specific handler classes and handler methods.
  • @PostMapping - shortcut for @RequestMapping(method = RequestMethod.POST). There is also @GetMapping, @PutMapping, @DeleteMapping
  • @RequestParam - get the parameters in the request URL
  • @RequestBody - method parameter should be bound to the value of the HTTP request body.
  • @PathVariable - used to handle dynamic changes in the URI where a certain URI value acts as a parameter.

Sample Application

The sample application demos most of the commonly used annotations in a Spring Boot web application

Entity class Task

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Data
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

String description;

boolean done;
}

MyController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@RestController
@RequestMapping("api/tasks")
class TaskController {

@Autowired
TaskRepository repository;

@GetMapping
public ResponseEntity<List<Task>> getAll() {
try {
List<Task> tasks = new ArrayList<Task>();

repository.findAll().forEach(tasks::add);

if (tasks.isEmpty())
return new ResponseEntity<>(HttpStatus.NO_CONTENT);

return new ResponseEntity<>(tasks, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@GetMapping("/{id}")
public ResponseEntity<Task> getById(@PathVariable("id") Long id) {
Optional<Task> existingtaskOptional = repository.findById(id);

if (existingtaskOptional.isPresent()) {
return new ResponseEntity<>(existingtaskOptional.get(), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

@GetMapping(value="/search", params = {"description", "done"})
public ResponseEntity<List<Task>> getByDescriptionAndDone(
@RequestParam(value="description", required = true) String description,
@RequestParam(value="done", required = false, defaultValue = "false") boolean done) {
List<Task> tasksFound = repository.findByDescriptionAndDone(description, done);
return ResponseEntity.ok(tasksFound);
}

@PostMapping
public ResponseEntity<Task> create(@RequestBody Task task) {
try {
Task savedtask = repository.save(task);
return ResponseEntity.status(HttpStatus.CREATED).body(savedtask);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}

@PutMapping(
value = "/{id}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Task> update(@PathVariable("id") Long id, @RequestBody Task task) {
Optional<Task> existingtaskOptional = repository.findById(id);
if (existingtaskOptional.isPresent()) {
Task existingtask = existingtaskOptional.get();
existingtask.setDescription(task.getDescription());
existingtask.setDone(task.isDone());
return new ResponseEntity<>(repository.save(existingtask), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

@DeleteMapping("/{id}")
public ResponseEntity<HttpStatus> delete(@PathVariable("id") Long id) {
try {
repository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
}
}
}

@RestController

A convenience annotation that is itself annotated with @Controller and @ResponseBody. This is used to indicate the class is used as Rest Controller.

@RequestMapping annotation can be used at the class level to map requests.

1
2
3
4
@RestController
@RequestMapping("api/tasks")
class TaskController {
}

@GetMapping

You can use @RequestMapping to map a GET request. However, it is often more convenient to use @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, or @PatchMapping.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping
public ResponseEntity<List<Task>> getAll() {
try {
List<Task> tasks = new ArrayList<Task>();

repository.findAll().forEach(tasks::add);

if (tasks.isEmpty())
return new ResponseEntity<>(HttpStatus.NO_CONTENT);

return new ResponseEntity<>(tasks, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PathVairiable

Use @PathVariable to bind parameter to path variable.

1
2
3
4
5
6
7
8
9
10
@GetMapping("/byId/{id}")
public ResponseEntity<Task> getById(@PathVariable("id") Long id) {
Optional<Task> existingtaskOptional = repository.findById(id);

if (existingtaskOptional.isPresent()) {
return new ResponseEntity<>(existingtaskOptional.get(), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

@RequestParam

Use @RequestParam to bind parameters to query parameters.

You can set if parameter is required and its default value for @RequestParam.

1
2
3
4
5
6
7
@GetMapping(value="/search", params = {"description", "done"})
public ResponseEntity<List<Task>> getByDescriptionAndDone(
@RequestParam(value="description", required = true) String description,
@RequestParam(value="done", required = false, defaultValue = "false") boolean done) {
List<Task> tasksFound = repository.findByDescriptionAndDone(description, done);
return ResponseEntity.ok(tasksFound);
}

@RequestBody

POST or PUT request may have request body, use @RequestBody to bind the parameter to request body

1
2
3
4
5
6
7
8
9
@PostMapping
public ResponseEntity<Task> create(@RequestBody Task task) {
try {
Task savedtask = repository.save(task);
return new ResponseEntity<>(savedtask, HttpStatus.CREATED);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

consumes and produces

consumes element narrows the primary mapping by media types that can be consumed by the mapped handler.
produces element narrows the primary mapping by media types that can be produced by the mapped handler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PutMapping(
value = "/{id}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Task> update(@PathVariable("id") Long id, @RequestBody Task task) {
Optional<Task> existingtaskOptional = repository.findById(id);
if (existingtaskOptional.isPresent()) {
Task existingtask = existingtaskOptional.get();
existingtask.setDescription(task.getDescription());
existingtask.setDone(task.isDone());
return new ResponseEntity<>(repository.save(existingtask), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

Return a ResponseEntity

You can create a ResponseEntity using new and return it.

1
2
3
4
5
6
7
8
9
@PostMapping
public ResponseEntity<Task> create(@RequestBody Task task) {
try {
Task savedtask = repository.save(task);
return new ResponseEntity<>(savedtask, HttpStatus.CREATED);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

Or you can use a Builder

1
2
3
4
5
6
7
8
9
@PostMapping
public ResponseEntity<Task> create(@RequestBody Task task) {
try {
Task savedtask = repository.save(task);
return ResponseEntity.status(HttpStatus.CREATED).body(savedtask);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}

Download files

Return value is org.springframework.core.io.Resource. You can use InputStreamResource or ByteArrayResource to return file.

Using InputStreamResource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Download file using InputStreamResource
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile() throws FileNotFoundException {
String filename = "abc.txt";
File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + filename);

InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=abc.txt");
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");

return ResponseEntity.ok()
.headers(headers)
.contentLength(file.length())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}

Use ByteArrayResource if byte array is already available.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Download image file using ByteArrayResource
@GetMapping("/downloadimage")
public ResponseEntity<Resource> downloadImage() throws IOException {
String filename = "star.jpeg";
File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + filename);

ByteArrayResource resource = new ByteArrayResource(FileUtils.readFileToByteArray(file));

HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=star.jpeg");
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");

return ResponseEntity.ok()
.headers(headers)
.contentLength(file.length())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}

Another option is to write to copy to the response output stream.

1
2
3
4
5
6
7
8
9
10
@GetMapping(path = "/fileoutput", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<?> fileoutput(HttpServletResponse response) throws Exception {
String filename = "abc.txt";
File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + filename);

InputStream yourInputStream = new FileInputStream(file);
IOUtils.copy(yourInputStream, response.getOutputStream());
response.flushBuffer();
return ResponseEntity.ok().build();
}

Unit Test using @WebMvcTest

Use @WebMvcTest annotation to test a controller without the full application configuration.

By default @WebMvcTest auto configures the Controller and MockMvc. You can use @MockBean to create beans required by the Controller.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebMvcTest(TaskController.class)
public class TaskControllerTest {

@MockBean
private TaskRepository repository;

@Autowired
private MockMvc mockMvc;

@Test
public void findTask() throws Exception {
Task laundryTask = new Task();
laundryTask.setDescription("Laundry");
when(repository.findAll()).thenReturn(List.of(laundryTask));
this.mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].description").value("Laundry"));
}
}

If you want to only test a controller use @WebMvcTest annotation.

If you are looking to load your full application context and use MockMVC, you should consider @SpringBootTest combined with @AutoConfigureMockMvc rather than this annotation.

Integration Test

You can use @SpringBootTest to start a complete Spring Boot application with full app context.

Use @SpringBootTest for integration test. The initial data can be loaded using src/test/resources/data.sql file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles(profiles = "integration-test")
public class TaskControllerFullConfigTest {

@Autowired
private TaskRepository repository;

@Autowired
private MockMvc mockMvc;

@Test
public void findTask() throws Exception {
this.mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].description").value("Laundry"));
}
}

Reference