13 minutes read

Web-based applications communicate with a server via an API — various methods that can be called through HTTP (HyperText Transfer Protocol) requests. A controller is a part of the application that handles these API methods.

In this topic, we will look at how you can implement a basic REST-based controller for retrieving data through GET requests. The diagram below outlines the typical flow of a REST API when a GET request is sent to the server through Spring.

REST API flow diagram

Rest Controller

The @RestController annotation usually sits on top of a class. It makes a class provide exact endpoints (URLs) to access the REST methods. The class and its methods can tell which requests suit your case. All requests will be sent to an appropriate method of this class.

Suppose we want to create an API. When users access a specific URL, they should receive the response 1. To make this possible with Spring, we will use two annotations. The first is @RestController, which handles any REST requests sent to the application by a user. To create a REST controller, we should create a class and annotate it with @RestController:

Java
import org.springframework.web.bind.annotation.*;

@RestController
public class TaskController {

}
Kotlin
@RestController
class TaskController {

}

The @RestController annotation is a wrapper of two different annotations:

  1. @Controller contains handler methods for various requests. Since we opted for @RestController, the methods are related to REST requests.

  2. @ResponseBody binds the return value of each handler method to a web response body. They will be represented in JSON format. When we send a request, the response we receive is in JSON format. This will become clear when we start working with objects in our GET requests.

We can implement methods to handle various REST requests in classes annotated with @RestController. To implement a GET request, we can use the @GetMapping annotation. It indicates what URL path should be associated with a GET request. After that, we can implement a method to be executed when the GET request is received at that path. For example, we can create a GET request that returns 1 when http://localhost:8080/test is accessed:

Java
@RestController
public class TaskController {

    @GetMapping("/test")
    public int returnOne() {
        return 1;
    }
}
Kotlin
@RestController
class TaskController {
    @GetMapping("/test")
    fun returnOne(): Int {
        return 1
    }
}

When you send a request to http://localhost:8080/test, you will receive 1 in return.

hhtp response with 1 in body

In addition to using Postman, you can send GET requests to a server through a browser. To do so, open your browser, and navigate to the same URL you would use with Postman (in this example, http://localhost:8080/test).

GET with Collections

A list is an excellent way to store data. Sometimes, we want to return a complete list or a specific element of that list when a GET request is received. We can adjust our @GetMapping annotation to do so.

First, we need to create objects to store in our list. Let's call their class Task. It will implement a basic constructor as well as getters and setters for each of the object properties:

Java
public class Task {
    private int id;
    private String name;
    private String description;
    private boolean completed;

    public Task() {}

    public Task(int id, String name, String description, boolean completed) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.completed = completed;
    }

    // getters and setters
}
Kotlin
class Task(
    var id: Int,
    var name: String,
    var description: String,
    var completed: Boolean
) {

}

It is essential to implement getters and setters. If they are not implemented, Spring cannot display object contents correctly. Spring will try to return all data from our controller in JSON format or similar. To construct a readable representation of our object, Spring needs getters and setters to access the object properties.

After that, we can implement a collection to store our tasks. We are going to use a list. When we work with Spring, we can face a lot of GET requests at the same time. So, using an immutable collection to eliminate any thread-based issues would be a good idea. We also need to make sure that our application can use our collection:

Java
@RestController
public class TaskController {
    private final List<Task> taskList = List.of(
            new Task(1, "task1", "A first test task", false),
            new Task(2, "task2", "A second test task", true)
    );
}
Kotlin
@RestController
class TaskController {
    val taskList = listOf(
        Task(1, "task1", "A first test task", false),
        Task(2, "task2", "A second test task", true)
    )
}

In the snippet above, we created the Task list and populated it with sample tasks. If you need, you can start working with the objects from a database query instead, though. Now we need to create a @GetMapping function that can be used to retrieve data from the tasks collection.

Java
@RestController
public class TaskController {
    private final List<Task> taskList = List.of(
            new Task(1, "task1", "A first test task", false),
            new Task(2, "task2", "A second test task", true)
    );

    @GetMapping("/tasks")
    public List<Task> getTasks() {
        return taskList;
    }
}
Kotlin
@RestController
class TaskController {
    val taskList = listOf(
        Task(1, "task1", "A first test task", false),
        Task(2, "task2", "A second test task", true)
    )

    @GetMapping("/tasks")
    fun getTasks(): List<Task> {
        return taskList
    }
}

When we make a GET request to http://localhost:8080/tasks, we will see all tasks that were added earlier:

http response with list of tasks

In addition to a List, we can return other collections from a RestController. As with a list, a Set is converted to a JSON array. However, a Map is converted to a JSON key-value structure.

@PathVariable

We may want to modify the above code so that users can enter an ID to specify which task they want to retrieve. To do this, we must add a @PathVariable annotation to a mapping method. The code below shows how we can add an ID to our getTask function:

Java
@RestController
public class TaskController {
    private final List<Task> taskList = List.of(
        new Task(1, "task1", "A first test task", false),
        new Task(2, "task2", "A second test task", true)
    );

    @GetMapping("/tasks/{id}")
    public Task getTask(@PathVariable int id) {
        return taskList.get(id - 1); // list indices start from 0
    }
}
Kotlin
@RestController
class TaskController {
    val taskList = listOf(
        Task(1, "task1", "A first test task", false),
        Task(2, "task2", "A second test task", true)
    )

    @GetMapping("/tasks/{id}")
    fun getTask(@PathVariable id: Int): Task? {
        return taskList[id - 1] // list indices start from 0
    }
}

We added {id} to the @GetMapping annotation to tell Spring we expect the id parameter. We also placed the id variable as @PathVariable in the arguments of our getTask method. This annotation indicates how Spring should map the parameter in @GetMapping to the method. After that, the method will return only one element rather than the whole collection. A request to http://localhost:8080/tasks/1 gives us the first task in the list:

http response with task with a given id

However, if we provide an invalid ID, the getTask method will throw an exception, and we will receive a 500 error, similar to what is pictured below:

http response with error 500

Customizing the status code

By default, a method annotated with @GetMapping returns a response with the status code 200 OK if a request was processed successfully and the status code 500 if there is an uncaught exception. However, we can change this default status code by returning an object of the ResponseEntity<T> class.

In the example below, we return 202 ACCEPTED instead of 200 OK.

Java
@GetMapping("/tasks/{id}")
public ResponseEntity<Task> getTasks(@PathVariable int id) {
    return new ResponseEntity<>(taskList.get(id - 1), HttpStatus.ACCEPTED);
}
Kotlin
@GetMapping("/tasks/{id}")
fun getTasks(@PathVariable id: Int): ResponseEntity<Task?>? {
    return ResponseEntity(taskList[id - 1], HttpStatus.ACCEPTED)
}

The status code 202 ACCEPTED is not the best example for this case, but it demonstrates the possibility of changing the status code.

Read more on this topic in Beginners Guide to REST API Testing on Hyperskill Blog.

Conclusion

A controller is the first component that meets and greets a web request. In this topic, we have covered defining a method handling GET requests in a @RestController-annotated class to receive data from a web application. This request type is common in APIs and is often required to return sets or single elements.

On the one hand, web app developers must keep request handlers short and clear to help find the correct handler and quickly create valid requests. On the other hand, web apps are clients of other web apps. This means they can call controllers of other applications. That's why you also need to know foreign handlers to determine what requests they can handle.

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