Building a CRUD RESTful API/Web Service with Spring Boot

Overview

This guide aims to help you create from scratch a CRUD RESTful API with Spring Boot. We’ll create an User entity and develop its endpoints accordingly with a Rest Controller and a Service class.

To learn more about HTTP methods click here , and RESTful APIs, click here.

Later on we’ll be using this project as a base and evolve it, adding new functionalities.

We’ll cover the following topics in this tutorial :

  • Building our User class with its own repository and particular attributes.
  • Creating CRUD (Create, Read, Update, Delete) endpoints for our User class with a respective Controller and Service.
  • Differences between GET, POST, PUT and DELETE from HTTP request methods.
  • Meaning of all annotations used to make the code cleaner, easier to read and easier to maintain.

Tools used to make this tutorial :

  • IntelliJ as our IDE of choice;
  • Maven 3.0+ as build tool;
  • JDK 1.8+;

Getting started

We’ll first need to bootstrap our project with Spring Initializr.

This can be done by going to http://start.spring.io/ or if you’re using IntelliJ, by creating a New Project and choosing Spring Initializr instead of the standard Java option in the quick menu.

Either way, we’ll configure our project the following way :

Initial configuration of a Spring project in IntelliJ

The next step is selecting our chosen dependencies to work with, so be sure to include :

  • Lombok : Java library that makes the code cleaner and gets rid of boilerplate code;
  • Spring WEB : Product of the Spring community focused on creating document-driven Web services;
  • Rest Repositories HAL Browser : Provides an in-browser GUI to traverse our REST API;
  • Spring Data JPA : Provides repository support for the Java Persistence API (JPA);
  • H2 Database : Our relational database management system written in Java.

This step can also be done by writing the dependencies in the pom.xml file present in our project folder :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.users-api</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Users API</name>
    <description>CodeFiction Spring API Project</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-rest-hal-browser</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Creating our User entity class and UserRepository interface

The first step coding-wise, is to create our User class.

src/main/java/com/usersapi/domain/user/User.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

}

One thing that may come up to you as unclear, is why are we implementing serialization in our User class?

It’s due to the fact that serialization converts an object into a sequence of bytes which can then be saved to a database or transferred over a network. When working with our User entity, this may come in handy later on.

Notes about the annotations :

When working with JPA as our persistence layer, it’s necessary to map our API to persist our objects of choice.

  • @Entity : Tells that our class can be represented by a table in the database, with each instance representing a row.
  • @Id : Specifies that this attribute is the primary key of this entity.
  • @GeneratedValue(strategy = GenerationType.IDENTITY) : Informs the persistence layer that the strategy used to generate the value of our primary key is by auto-increasing when a new user is created.

As for the rest, this is when Lombok starts to show up in our application.

  • @Data : Creates toString, equals, hashCode, getters and setters methods for our class.
  • @AllArgsConstructor : Creates a class constructor with all arguments.
  • @NoArgsConstructor : Creates an empty class constructor with all arguments.

Next step is creating our UserRepository interface which will extend JpaRepository to access the methods necessary to manipulate the User table.

We’ll also need to pass the class that represents our model and the type of the primary key as generic arguments.

src/main/java/com/usersapi/domain/user/UserRepository.java

@Repository
public interface UserRepository extends JpaRepository<User, Long> {}

The @Repository annotation is from Spring and its purpose is simply to indicate that the class provides the mechanism for storage, retrieval, search, update and delete operation on objects.

Configuration of our in-memory database and inputting initialization example data into User table

In our src/main/resources/application.properties file, we’ll need to specify the data source for our Spring application.

spring.datasource.url=jdbc:h2:mem:usersapi

Some notes on what we are doing :

  • jdbc : Stands for Java Database Connectivity, it’s a set of class and interfaces written in Java to send SQL instructions to any operational database.
  • h2 : Our in-memory database of choice.

To create initialization example data for our User class, we’ll need to create a data.sql file and input our data there.

src/main/resources/data.sql

INSERT INTO user(name) VALUES('Neo');
INSERT INTO user(name) VALUES('Owt');
INSERT INTO user(name) VALUES('Three');

COMMIT;

Creating CRUD endpoints

For each endpoint, there will be a Rest Controller and a Service class to implement its logic. Our endpoints will be :

  • Create : When given a valid HTTP POST request in /users, create a specific user.
  • Detail : When given a valid HTTP GET request in /users/{id}, retrieve the details of a specific user by its id.
  • List : When given a valid HTTP GET request in /users, retrieve the details of all users.
  • Update : When given a valid HTTP PUT request in /users/{id}, update the details of a specific user by its id.
  • Delete : When given a valid HTTP DELETE request in /users/{id}, delete a specific user by its id.

Creating a new user with the POST request

Since our Service class is the one that will implement the logic of our controller, we might as well start with it :

src/main/java/com/usersapi/endpoints/create/CreateUserService.java

@Service
public class CreateUserService {

    @Autowired
    UserRepository repository;

    public User createNewUser(User user) {
        return repository.save(user);
    }
}
  • @Service : This annotation is a specialization of the @Component annotation, it is used to mark the class as a service provider.
  • @Autowired : Our dependency injection annotation, autowiring our application is fundamental when building it.

As for our controller :

src/main/java/com/usersapi/endpoints/create/CreateUserController.java

@RestController
@RequestMapping("/users")
public class CreateUserController {

    @Autowired
    CreateUserService service;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<User> createNewUser_whenPostUser(@RequestBody User user) {

        User createdUser = service.createNewUser(user);

        URI uri = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(createdUser.getId())
                .toUri();

        return ResponseEntity.created(uri).body(createdUser);
    }
}
  • @RestController : Combines the @Controller and @ResponseBody annotations, which eliminates the need to annotate every request handling method of the controller class with @ResponseBody.
  • @RequestMapping(“/users”) : The main purpose of this annotation here is implementing our URL handler but it could also map our HTTP request onto our method if it wasn’t already being done by the @PostMapping.
  • @PostMapping : Maps our POST request onto our method.
  • @ResponseStatus : Straightforward way to set the status code of our HTTP response.
  • @RequestBody : Ties our method parameter to the body of our HTTP request. This annotation indicates that the return type should be written straight to the HTTP response body (and not interpreted as a view name).

Building the structure of our GET requests : Detail and List

When retrieving the details of one specific user, we’ll need to fetch him by its id and manage the error possibility caused by a scenario that he doesn’t exist.

src/main/java/com/usersapi/endpoints/detail/DetailUserService.java

@Service
public class DetailUserService {

    @Autowired
    UserRepository repository;

    public Optional<User> listUser(Long id) {

        Optional<User> user = repository.findById(id);
        if (!user.isPresent()) {
            throw new UserNotFoundException(id);
        } else {
            return repository.findById(id);
        }
    }
}

No new annotations here. Our UserNotFoundException method will come from the following :

src/main/java/com/usersapi/endpoints/detail/UserNotFoundException.java

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(Long id) {
        super("Could not find user with id " + id + ".");
    }
}

When an UserNotFoundException is thrown, this extra tidbit of Spring MVC configuration will be used to render an HTTP 404 response.

src/main/java/com/usersapi/endpoints/detail/UserNotFoundAdvice.java

@ControllerAdvice
public class UserNotFoundAdvice {

    @ResponseBody
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    String userNotFoundHandler(UserNotFoundException ex) {
        return ex.getMessage();
    }
}
  • @ControllerAdvice : Intercepts our exception.
  • @ExceptionHandler : Configures the advice to only respond if an UserNotFoundException is thrown.
  • @ResponseBody : Signals that this advice is rendered straight into the response body.

As for our Controller, if the contact is found, we’ll return it with a HTTP 200 response.

src/main/java/com/usersapi/endpoints/detail/DetailUserController.java

@RestController
@RequestMapping("/users/{id}")
public class DetailUserController {

    @Autowired
    DetailUserService service;

    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<Optional<User>> listUser_whenGetUser(@PathVariable Long id) {
        return ResponseEntity.ok().body(service.listUser(id));
    }
}
  • @GetMapping : Similar to the @PostMapping annotation, maps our GET request onto our method.
  • @PathVariable : Binds method parameter id with the path variable /{id}.

When listing or retrieving all users, no exceptions will need to be handled, so this can be done by simply calling the repository.

src/main/java/com/usersapi/endpoints/list/ListUserService.java

@Service
public class ListUserService {

    @Autowired
    UserRepository repository;

    public List<User> listAllUsers() {
        return repository.findAll();
    }
}

src/main/java/com/usersapi/endpoints/list/ListUserController.java

@RestController
@RequestMapping("/users")
public class ListUserController {

    @Autowired
    ListUserService service;

    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<List<User>> listAllUsers_whenGetUsers() {
        return ResponseEntity.ok().body(service.listAllUsers());
    }
}

Updating an existing user with the PUT request

When updating an user by its id, we need to check if it exists the same way we did with the listUser method.

Next, we’ll find the user that needs to be updated and save it with its new parameters. Our Service class should be something like :

src/main/java/com/usersapi/endpoints/update/UpdateUserService.java

@Service
public class UpdateUserService {

    @Autowired
    UserRepository repository;

    public User updateUser(Long id, User user) {

        Optional<User> userOptional = repository.findById(id);
        if (!userOptional.isPresent()) {
            throw new UserNotFoundException(id);
        } else {
            repository.findById(id);
            return repository.save(user);
        }
    }
}

As for building our Controller it should be pretty straightforward by now, since our Service contains the logic behind our endpoint implementation.

All we need to do is pass our soon to be changed user and its id as parameters, also the @PutMapping annotation to handle the HTTP request.

src/main/java/com/usersapi/endpoints/update/UpdateUserController.java

@RestController
@RequestMapping("/users/{id}")
public class UpdateUserController {

    @Autowired
    UpdateUserService service;

    @PutMapping
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<User> updateUser_whenPutUser(@RequestBody User user, @PathVariable Long id) {
        return ResponseEntity.ok().body(service.updateUser(id, user));
    }
}

Removing an user from the database with the DELETE request

Removing an existing user with the delete request method is probably the simplest one. All we have to do is call the repository to delete an user by its id in case it’s found.

In case it doesn’t exist, we’ll need to throw our UserNotFoundException method.

src/main/java/com/usersapi/endpoints/delete/DeleteUserService.java

@Service
public class DeleteUserService {

    @Autowired
    UserRepository repository;

    public void deleteUser(Long id) {
        Optional<User> userOptional = repository.findById(id);
        if (!userOptional.isPresent()) {
            throw new UserNotFoundException(id);
        } else {
            repository.deleteById(id);
        }
    }
}

src/main/java/com/usersapi/endpoints/delete/DeleteUserController.java

@RestController
@RequestMapping("/users/{id}")
public class DeleteUserController {

    @Autowired
    DeleteUserService service;

    @DeleteMapping
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser_whenDeleteUser(@PathVariable Long id) {
        service.deleteUser(id);
    }
}

Endnotes

Throughout this tutorial, you have engaged in various steps to build a RESTful API/Web Service with CRUD operations, our next steps with this will be :

  • Testing the execution of our endpoints with Postman.
  • Implementing Swagger in our API for documentation and quick interface purposes.
  • Unit testing our endpoints controllers and services with JUnit and Mockito.
  • Integration testing our whole API.

Source code

Available on our github page here.

Thanks for reading !! Feel free to leave any comment.

About the author

Website | + Posts

Software Consultant with more than 11 years experience, most of that in Finance area. I love building things and see them running and making a difference.

Specialised in Golang (2 years), Java (11 years), Javascript, React and Angular (1 year), PHP (2 years), project management and delivery leading (2 years), mentoring and coaching (1 year).

Website | + Posts

Electrical engineering student from Brazil passionate about learning and teaching people.

Had some professional and academic engineering experiences, now my focus is on studying and developing software as a full time job.

Leave a Reply

Your email address will not be published. Required fields are marked *