Published on

Part 2: Let’s build a simple spring boot REST API - Set up a controller

Authors

When it comes to REST API, a controller is a component that controls the flow of the data both from the user and the application itself. In other words, controllers accept an HTTP request and pass the request to the services of the application and return the output from the services to the user. This is why a controller is one of the most important components of the application. In this part 2, we are going to set up a controller and create a REST API with a basic CRUD process.

Previous Chapter

https://www.juniordevmind.com/blog/1-Lets-build-a-simple-spring-boot-REST-API-Initialize-a-spring-boot-project

Source code for this article

https://github.com/ryuichi24/simple-spring-rest-api/tree/3-setup-controller

Project structure

simplespringrestapi
├── SimpleSpringRestApiApplication.java
├── controllers
│   └── TodoController.java
└── models
    └── TodoItem.java

TodoController.java

package tech.ryuichi24.simplespringrestapi.controllers;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import tech.ryuichi24.simplespringrestapi.models.TodoItem;

@RequestMapping(path = TodoController.BASE_URL)
@RestController
public class TodoController {
    public static final String BASE_URL = "/api/v1/todos";
    private final AtomicInteger _counter = new AtomicInteger();
    private final List<TodoItem> _todoItems = new ArrayList<>() {
        {
            add(new TodoItem(_counter.incrementAndGet(), "todo 1"));
            add(new TodoItem(_counter.incrementAndGet(), "todo 2"));
            add(new TodoItem(_counter.incrementAndGet(), "todo 3"));
        }
    };

    @RequestMapping(method = RequestMethod.GET, path = "")
    public ResponseEntity<List<TodoItem>> getTodoItems() {
        return ResponseEntity.ok(_todoItems);
    }

    @RequestMapping(method = RequestMethod.GET, path = "/{id}")
    public ResponseEntity<TodoItem> getTodoItem(@PathVariable int id) {
        TodoItem found = _findTodoItemById(id);
        if (found == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not Found");
        }

        return ResponseEntity.ok(found);
    }

    @RequestMapping(method = RequestMethod.POST, path = "")
    public ResponseEntity<TodoItem> createTodoItem(@RequestBody TodoItem newTodoItem) {
        if (newTodoItem == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Todo item must not be null.");
        }

        newTodoItem.setId(_counter.incrementAndGet());
        _todoItems.add(newTodoItem);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
                .buildAndExpand(newTodoItem.getId()).toUri();
        return ResponseEntity.created(location).body(newTodoItem);
    }

    @RequestMapping(method = RequestMethod.PUT, path = "/{id}")
    public ResponseEntity<?> updateTodoItem(@PathVariable int id, @RequestBody TodoItem newTodoItem) {
        TodoItem found = _findTodoItemById(id);
        if (found == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not Found");
        }

        _todoItems.remove(found);
        _todoItems.add(newTodoItem);

        return ResponseEntity.noContent().build();
    }

    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public ResponseEntity<?> removeTodoItem(@PathVariable int id) {
        TodoItem found = _findTodoItemById(id);
        if (found == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not Found");
        }
        _todoItems.remove(found);
        return ResponseEntity.noContent().build();
    }

    private TodoItem _findTodoItemById(int id) {
        return _todoItems.stream().filter(item -> item.getId() == id).findAny().orElse(null);
    }
}
simplespringrestapi
├── SimpleSpringRestApiApplication.java
├── controllers
│   └── TodoController.java # <- this one!
└── models
    └── TodoItem.java

TodoController.java has 5 endpoints, which are getTodoItems, getTodoItem, createTodoItem, updateTodoItem, and removeTodoItem. Each method interacts with _todoItems array list to achieve the CRUD operation of todo items.

Spring Boot takes an advantage of annotations a lot, which makes its code much more clean. For the TodoController.java, I used 4 types of annotations.

@RestController

@RestController // <- this one
public class TodoController {
	# ommited...
}

A class annotated with this gets automatically registered in the dependency container of Spring Boot, and it will be instantiated automatically on runtime. Every controller must have this annotation.

@RequestMapping

@RequestMapping(method = RequestMethod.GET, path = "/{id}") // <- this one!
public ResponseEntity<TodoItem> getTodoItem(@PathVariable int id) {
    TodoItem found = _findTodoItemById(id);
    if (found == null) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not Found");
    }

    return ResponseEntity.ok(found);
}

With this annotation, you can specify an HTTP method and a path of the endpoint. There are other annotations that can specify the same params such as @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, and @PatchMapping.

@RequestBody

@RequestMapping(method = RequestMethod.POST, path = "")
public ResponseEntity<TodoItem> createTodoItem(@RequestBody TodoItem newTodoItem) { // <- this one!
    if (newTodoItem == null) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Todo item must not be null.");
    }

    newTodoItem.setId(_counter.incrementAndGet());
    _todoItems.add(newTodoItem);
    URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
            .buildAndExpand(newTodoItem.getId()).toUri();
    return ResponseEntity.created(location).body(newTodoItem);
}

This annotation automatically maps the content of the HTTP request with the specified type. In this case, the body will be mapped with TodoItem.

@PathVariable

@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
public ResponseEntity<?> removeTodoItem(@PathVariable int id) { // <- this one!
    TodoItem found = _findTodoItemById(id);
    if (found == null) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not Found");
    }
    _todoItems.remove(found);
    return ResponseEntity.noContent().build();
}

This annotation maps the path parameter specified in the path argument of @RequestMapping with the annotated argument. In this case, the value of id annotated with @PathVariable comes from the path parameter specified as /{id}.

TodoItem.java

package tech.ryuichi24.simplespringrestapi.models;

public class TodoItem {
    private int id;
    private String title;

    public TodoItem() {
    }

    public TodoItem(int id, String title) {
        this.id = id;
        this.title = title;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}
simplespringrestapi
├── SimpleSpringRestApiApplication.java
├── controllers
│   └── TodoController.java
└── models
    └── TodoItem.java # <- this one!

This is a quite basic Java class. One thing to mention here is that it has two constructors, which are one with an empty argument constructor and the other having id and title arguments. The reason why it has an empty constructor is Spring Boot can instantiate this class without any arguments.

@RequestMapping(method = RequestMethod.POST, path = "")
    public ResponseEntity<TodoItem> createTodoItem(@RequestBody TodoItem newTodoItem) {
				// ommited...
    }

This is why Spring Boot can automatically map the content of the body coming from the client with the specified argument annotated with @RequestBody.

Test API

Let’s run the API and test each endpoint with the curl commands like below:

getTodoItems

curl -s -X GET http://localhost:8080/api/v1/todos | xargs echo

geTodoItem

curl -s -X GET http://localhost:8080/api/v1/todos/1 | xargs echo

createTodoItem

curl -s -X POST -d '{ "title": "todo 4" }' -H "Content-Type: application/json" http://localhost:8080/api/v1/todos | xargs echo

updateTodoItem

curl -s -X PUT -d '{ "title": "todo 4 updated" }' -H "Content-Type: application/json" http://localhost:8080/api/v1/todos/1 | xargs echo

removeTodoItem

curl -s -X DELETE http://localhost:8080/api/v1/todos/1 | xargs echo

Add swagger UI

Testing one by one with curl is fine, but as the project gets bigger and more complicated , it gets harder to test it. So most of the time, developers start using API testing tools such as “Postman”, which I am a huge fan of. But I am too lazy to make each testing request in the Postman, so I will use “Swagger-UI”, which automatically generates an HTML page where you can test the API. Since the page gets created based on the implementation of the API, you do not have to make a testing request one by one; Swagger-UI does everything for you.

Adding “Swagger-UI” to Spring Boot is quite simple; you just have to add a new dependency in dependencies section of the build.gradle file like below:

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' # <- new!
}

After re-building the project, you can go to http://localhost:8080/swagger-ui/index.html from your browser.

Conclusion

We set up a controller to make a REST API. A controller is responsible only for the flow of data between the client and its application or service, but this time all business logic was written in the controller. This is why next time, we will introduce a service class that comprehends the business logic.

References

How to Return HTTP Status Codes in a Spring Boot Application

Example usage for org.springframework.http ResponseEntity created

Spring @RequestBody without using a pojo?

How to return 404 response status in Spring Boot @ResponseBody - method return type is Response?

add location header to Spring MVC's POST response?