Getting Started with Spring Boot

Table Of Contents

In this article, we will build a production-grade application with Spring Boot.

After understanding the use case and requirements, we will implement the application layer by layer.

Let us dive into the requirements for the application.

Example Code

This article is accompanied by a working code example on GitHub.

Requirements

We need to build an application for the local bookstore that will allow them to keep track of borrowed books.

The bookstore wants to have these functionalities in the application:

  • The application should be accessible through a web browser.
  • The user has to provide their name, last name, email, and password.
  • Each book has information about an author, date of publication, and the number of copies (instances) in the bookstore.
  • The user can see all available books.
  • The user can borrow a book.
  • Each user can borrow only three books at one point in time.
  • The admin user can see all users that currently own a specific book.
  • The admin user can add a new book into the bookstore.
  • The admin user can delete a book from the bookstore.
  • The admin user can update information about a book in the bookstore.

High-level Architecture

Let’s take a look at a high-level architecture diagram to see how the application behaves:

Spring Boot High-level arch

Typing www.bookstore.loc in the browser will guide the user to the homepage. The homepage triggers the request to load all books available in the bookstore.

Inside the Spring Boot Application box, we can see layers we will implement through this article.

Controller classes accept the request from the browser and send it further down layers.

Methods inside the service layers contain all business logic. We will implement most requirements from the chapter above in this layer.

The service methods contact the repository layer to access the database.

We store data about books and users in the in-memory H2 database.

Setting up the Project

Spring provides the Spring Initializr project, which creates an application skeleton for us. The generated application eases the configuration phase and allows us to dive straight into the code.

We will look at two ways of creating a new Spring Boot project:

Both ways use the Spring Initializr project underneath and you can choose whichever way works best for you.

Creating the Project with Spring Initializr

On the Spring Initilizr page, we can create a new Spring Boot project:

Spring Boot initialization online

On this page, we provide the metadata about the application. After defining all necessary information, we can go and add our dependencies.

By clicking on the button on the top right corner, we can see the dependency selection screen:

Spring Boot initialization online

We will choose these three dependencies:

After selecting desired dependencies, we can generate our project by clicking the “Generate” button on the lower-left corner of the screen. Clicking the button will download the zip file onto our machine. After unpacking the zip and importing the project into IDE, we can start developing.

Creating the Project with IntelliJ

In the IntelliJ IDE, we can go to the File -> New -> Project…. We will get the next screen:

Spring Boot initialization through IDE

After defining the project on this screen, we can move forward to the next screen:

Spring Boot initialization through IDE

We choose the same dependencies as above and click the finish button to create the project and start developing.

Project Dependencies

Let’s learn a bit about the dependencies that we selected for our new project.

The Spring Web Dependency

The Spring Web dependency gives us everything we need to build a Spring MVC application.

Spring MVC is Spring’s implementation of the Model-View-Controller design pattern. A controller is the front part of the application that takes incoming requests and relays them to the right destination. Model is the object or collection that holds our data, and view represents the pages that the browser renders.

Let us look into the high-level architecture again and determine what does Spring Web provides:

Spring Boot High-level arch

The Spring Web dependency provides core Spring features (Inversion of Control, Spring MVC, server container for local running, etc.). With the Spring Web dependency, we can create controller classes from the image above.

The controller does several things:

  • Accepts incoming HTTP requests
  • Validates and deserializes input
  • Sends the data to the business logic layer
  • Deserializes output from the business logic layer
  • Handles exceptions and returns a correct response to the user

If we run the application, we will see that we can access the application on http://localhost:8080. We won’t see much, but the application will be up and running.

If you want to dive deeper into the responsibilities of a web controller and how to write tests for them, have a look at the article on testing MVC web controllers.

The Spring Data JPA Dependency

Building the data access layer can be cumbersome, and Spring Data JPA data gives us everything we need to start communicating with the database.

JPA (Java Persistence API) is a set of concepts that helps us write code against a database. Since JPA is just a set of specifications, it requires an implementation like Hibernate ORM.

Hibernate is an ORM (Object/Relational Mapping) solution. Hibernate takes care of mapping between Java classes and tables in the database. It allows us to edit data in tables without writing any SQL code.

Let us look into the application diagram to see which parts Spring Data JPA supports:

Spring Boot High-level arch

With Spring Data JPA, we can use the @Entity annotation to create database entities.

The database entity is the direct connection between the application and the table in the database. Using annotations like @ManyToMany, @Column, etc., we can define relationships between tables and constraints on columns.

Spring Data JPA also provides the repository interfaces:

  • CrudRepository
  • PagingAndSortingRepository
  • JpaRepository

Repositories are interfaces that hide the logic required for accessing the database. By extending the interface, we can run queries on the database without worrying about connection details.

We will talk more about this when we start building the repository layer.

If you want to dive deeper into Spring Data JPA repositories and how to write tests for them, have a look at the article on testing JPA queries.

Spring Data JPA Alternatives

Spring Data JPA is only one of many different alternatives of data access layers.

To read more about alternatives to Spring Data JPA, like Spring Data JDBC or Spring Data Neo4J please refer to the official page.

The H2 Database Dependency

The in-memory H2 database is excellent for fast iteration when we don’t care that the data is lost when we shut down the application.

Spring Boot High-level arch

This dependency offers us the ability to define the H2 database shown in the image above. We can define it as an in-memory or file-based database.

In the development environment, when we want to keep the data for easier access and testing different use cases, we want to configure the file-based database. The data will remain permanent at the desired location. We can access the data each time we start the application.

The H2 database

We should use the H2 database only in the prototyping phase. For later development and production, we should use something more stable and production-ready (e.g. Oracle, PostgreSQL, MySQL, etc.)

Project Files

The initialization process creates several files and folders. One of those files is the pom.xml.

The pom.xml defines all dependencies we are using in our project. Each dependency has its pom.xml. The inner pom.xml declares what does it bring into the application.

The dependency is the package that contains a piece of code that our project needs to run successfully.

Let us look into the pom.xml that Spring Boot generated for us:

<?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.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.reflectoring</groupId>
    <artifactId>beginners-guide</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>beginners-guide</name>
    <description>beginners-guide</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>2.6.3</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>2.1.210</version>
        </dependency>
    </dependencies>

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

We can see that most of our dependencies have the keyword starter. The starter keyword means that this dependency is built for Spring Boot and comes with pre-made configurations that we can use out of the box. Before the starter dependency, the user had to provide all dependencies manually. Also, we had to create our configuration for most things. The new approach helps us to start the development process much faster.

It is important to note that the configurations that come with starter dependencies are not invasive. We can create custom configurations only where we need them.

If you want to know more about Spring Boot starters, have a look at the quick guide to building a Spring Boot starter.

Building the Database Entities

Spring Boot High-level arch

A database entity represents the direct link with a database table. Entity classes represent columns, relationships between different tables, and constraints.

While creating database entities, we have to think about requirements from the beginning of the article:

  • The user has to provide their name, last name, email, and password.
  • Each book has information about the author, date of publication, number of instances, and users that currently own the book.
  • The user can see all books and borrow them.

After sketching which data tables we need, we will create those objects in the Java code.

Defining the Database Entities

Let’s take a look at the database diagram for our application:

Sketching the database entities

We have three tables in the database:

  • user
  • book
  • borrowed_books

The user table contains the columns id, name, lastname, email, and password. The id is our primary key, and the database will autogenerate it. We will see how to do it in the next chapter.

The book table has the columns id, title, author, publication, and numberOfInstances in the bookstore.

The borrowed_books table represents the many-to-many relationship between user and book. The many-to-many relationship means that one user can borrow several books and that one book can be borrowed by several users at the same time (given there are enough copies of the book).

Implementing the Book Entity

After we have defined the database, we can start implementing entity classes. The entity class is the direct connection to the table in the database:

@Entity(name = "book")
public class Book {

    //The rest of the class omitted

}

The @Entity annotation indicates that the annotated class is a JPA entity. The name attribute inside the annotation defines the table name. Setting the name is not mandatory, but if we do not set it, Spring will assume that the table name is the same as the class name.

Defining a Primary Key

When defining a class as an entity, we need to provide an id column using the @Id annotation. The id column is the primary key of that table.

@Entity(name = "book")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    //The rest of the class omitted

}

We need to decide how will the id column be generated. Using the @GeneratedValue annotation we can define several different strategies:

We used the AUTO strategy, which leverages whatever the database prefers. Most databases prefer to use the SEQUENCE strategy for their primary key definition.

IDENTITY Generation Strategy

The GenerationType.IDENTITY strategy allows the database to autoincrement the id value when we insert a new row. Let us look into the example of using the identity generation strategy:

@Entity(name = "book")
public class Book {

    @Id
    @GeneratedValue( strategy = GenerationType.IDENTITY)
    private long id;
    
    // Rest of the code omitted
}

We define the identity generation type in the strategy attribute of the @GeneratedValue. While the identity strategy is highly efficient for the database, it doesn’t perform well with the Hibernate ORM. Hibernate expects that every managed entity has its id set, so it needs to go and call the database to insert the id.

The identity strategy is excellent for fast iteration and the early stages. When we move to the development environment, we should move to something more stable and with better performance.

SEQUENCE Generation Strategy

The GenerationType.SEQUENCE strategy uses a database sequence to determine which primary key to select next. Let us look at how to define the primary key with sequence generators:

@Entity(name = "book")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, 
            generator = "book_generator")
    @SequenceGenerator(name = "book_generator",
            sequenceName = "book_seq",
            initialValue = 10)
    private long id;
}

To define the sequence generator, we annotate the field with the @SequenceGenerator annotation. We declare the name for Hibernate, the sequence name, and the initial value. After defining the generator, we need to connect it to the @GeneratedValue annotation by setting its name in the generator attribute.

TABLE Generation Strategy

The GenerationType.TABLE strategy uses a separate table to keep track of which primary key can be next:

@Entity(name = "book")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
            generator = "book_generator")
    @TableGenerator(name = "book_generator", table = "book_id_table")
    private long id;
}

We need to define the @TableGenerator annotation with the name and table attributes. We provide the generator name to the generator attribute inside the @GeneratedValue annotation.

Defining the Columns

We define each table column with the @Column annotation and the name of the column inside the name attribute:

@Entity(name = "book")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(name = "title")
    private String title;

    @Column(name = "author")
    private String author;

    @Column(name = "publication")
    private Date publication;

    @Column(name = "numberOfInstances")
    private int numberOfInstances;

    //The rest of the class omitted

}

If we do not provide the name attribute, Hibernate will assume that the column name is the same as the field name inside the Java class.

It is always better to be safe and set the name attribute so that things do not start crashing when someone accidentally changes a field name.

Defining the Many-to-Many Relationship

We define the many-to-many relationship between Book and User with the @ManyToMany annotation

Each relationship has two sides:

  • the owner - the table that has the foreign key
  • the target - the table to which one the owner is referring to with foreign key

The Owner of the Relationship

One side of that relationship needs to be the owning side and define the join table. We decided that it will be the User side:

@Entity(name = "_user")
public class User {
    // Rest of the code omitted

    @ManyToMany
    @JoinTable(
            name = "borrowed_books",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "book_id")
    )
    private List<Book> borrowedBooks;

    // Rest of the code omitted
}

After setting the @ManyToMany annotation, we need to define the table that will connect the user and book tables. In the @JoinTable annotation, we declare the name, foreign key, and inverse foreign key. We define the foreign and the inverse foreign key with the @JoinColumn annotation.

The Target Side

@Entity(name = "book")
public class Book {
    // Rest of the code omitted

    @ManyToMany(mappedBy = "borrowedBooks")
    private List<User> users;

    // Rest of the code omitted
}

We defined the target side with the @ManyToMany annotation with mappedBy attribute. We set the mappedBy attribute to the name on the owning side.

Configuring the Database

We said that the entity class represents the direct link to the table in the database. We have to define a configuration for database location, login information, etc. In Spring Boot applications, the application.properties file (or application.yml, if you prefer YAML over properties) is the place where we set that information.

Even though the Spring Boot framework comes with the configurations for most things, we need to tap in and change them. The excellent thing about provided configurations is that they are non-invasive, and we can change only those things that we need.

In this example, we will see how to configure the database. The H2 database can be an in-memory or persistent file-based database, and let us look at how to set them up.

Defining the Database URL

Let us look how we define the URL for the in-memory database.

spring.datasource.url=jdbc:h2:mem:localdb
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.datasource.driver-class-name=org.h2.Driver
# Rest of the configuration is omitted

After defining the keywords jdbc and h2 to note that we are using the H2 database, we are defining that we are using the in-memory database with the mem keyword. The localdb is the database name and can be whatever we want.

The in-memory database is good for fast iterations and prototyping, but we need something more persistent when we go into full development. Let us see how to define the file-based H2 database:

spring.datasource.url=jdbc:h2:file:/Users/mateostjepanovic/Documents/git/code-examples/spring-boot/beginners-guide/src/main/resources/data/demo;AUTO_SERVER=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.datasource.driver-class-name=org.h2.Driver
# Rest of the configuration is omitted

After defining the jdbc and the h2 keywords, we note that we want to use a file-based H2 database with the file keyword. The last part of the URL is the absolute path to the folder where we want to save our database.

The H2 database

Please note that we should use the H2 database only for the development phase. When going to the production environment, move to something more persistent and production-ready like Oracle, PostgreSQL, etc.

Defining the Database Login Credentials

After we defined the URL for the database, we need the login information for that database:

spring.datasource.username=username
spring.datasource.password=password
spring.h2.console.enabled=true

# Rest of the configuration is omitted

We define the username and password for the database.

Don’t Put Passwords in Your Configuration Files!

Please note that it’s fine to put the password in the configuration file only because it’s for an in-memory H2 database that is only used for local testing. In a production environment, the password and username should be externalized via environment variables or similar. Spring Boot supports first-class support for externalizing configuration.

Defining Schema Creation

We can set the configuration to autocreate the schema for us:

spring.jpa.hibernate.ddl-auto=create
spring.jpa.generate-ddl=true

# Rest of the configuration is omitted

Setting hibernate.ddl-auto to create, we tell Hibernate that we want to destroy and recreate the schema on each run. Only use this setting for testing!

Building the Data Access Layer

Spring Boot High-level arch

After creating entities, we can develop the data access layer. The data access layer allows us to use methods to manipulate the data in the database. We will build the data access layer using the repository pattern.

The repository pattern is the design pattern that leverages the usage of interfaces for connection to the database. The repository interface hides all implementation details of connecting to the database, maintaining the connection, transactions, etc.

Let us look at how to create the repository for the entity and explain how we use it.

Creating the Book Repository

public interface BookRepository extends JpaRepository<Book, Long> {
   // Rest of the code is omitted 
}

Extending JpaRepository turns our interface into the repository bean that is added to Spring’s ApplicationContext.

Spring’s ApplicationContext contains all the objects that make up our application. Objects in the ApplicationContext are called “beans” in Spring lingo. If an object is in the ApplicationContext, it can be injected into other beans and thus used by them.

The dependency injection is the pattern where objects do not construct dependencies they need but let the controller (in this case, Spring) do it.

When a class is a Spring Bean class, we are sure that we will get the instance where ever it is asked for using the @Autowired annotation, or one of the other ways of injection explained in the next chapter.

Our repository can extend several different interfaces:

  • CrudRepository
  • PagingAndSortingRepository
  • JpaRepository

The CrudRepository contains CRUD (Create, Read, Update, Delete) methods. It is the most basic one, and we should use it when we do not need anything besides those four methods.

The PagingAndSortingRepository extends the CrudRepository and adds some more functionality. Besides CRUD methods, we can fetch results in pages from the database and sort them with simple interface methods.

The JpaRepository extends the PagingAndSortingRepository. Except for methods from the PagingAndSortingRepository and the CrudRepository, we can flush and delete database records in batches.

Besides queries from the repository interface, we can create custom queries. We can do it in several ways:

Native SQL Queries

public interface BookRepository extends JpaRepository<Book, Long> {

    @Query(nativeQuery = true, value = "SELECT * FROM Book " +
            "book WHERE book.currentlyAvailableNumber > 5 ")
    List<Book> findWithMoreInstancesThenFive();
    
    // Rest of the code is omitted
}

We create the query by annotating the method with the @Query annotation. If we set the nativeQuery attribute to true, we can leverage the syntax from the underlying database.

Using Native Queries

Native queries are bound to a chosen database. If we use native queries, we lose one of the main advantages of using JPA, which abstracts away the database specifics. If you end up using a lot of native queries, you might be better off using a simpler abstraction like Spring Data JDBC.

JPQL Queries

If we don’t want to be bound to the syntax of the underlying database (maybe because we want to support multiple databases), we can use JPQL (Java Persistence Query Language) syntax:

public interface BookRepository extends JpaRepository<Book, Long> {

    @Query( value = "SELECT b FROM book b where b.numberOfInstances > 5")
    List<Book> findWithMoreInstancesThenFiveJPQL();

    // Rest of the code is omitted
}

We define the query with the @Query annotation, and the nativeQuery is, by default, set to false.

The JPQL syntax allows us to change the underlying database while the query stays the same.

Named Method Queries

Spring framework provides one more feature regarding queries. We can build queries by using a special naming convention for our repository methods:

public interface BookRepository extends JpaRepository<Book, Long> {

    List<Book> findAllByNumberOfInstancesGreaterThan(long limit);

    // Rest of the code is omitted
}

Using the attribute names and special keywords, we can create queries. Spring JPA will generate the proper queries according to our definition.

Building the Business Layer

The business layer is the core part of the application.

Spring Boot High-level arch

The business layer is where we should write the business logic. It should contain the code for the requirement that one user can borrow a maximum of three books. Each service class contains the business logic for part of the application.

We split our business layer in two ways. The first one is that each entity has its service class (e.g., BookService, UserService ). After we split the layer by entities, we split them by use case. The end product of splitting is that we have the GetBookService.java class. By reading the name of the class, we can conclude that the code for fetching books will be inside this class.

Defining the Service Class

One of the requirements is that the user can borrow three books maximum at one point in time. This check should be done in the business layer.

Let us look at how in the implementation class:

@Service
public class UpdateBookService {
    private final GetUserService getUserService;
    private final BookRepository bookRepository;

    public UpdateBookService(
            GetUserService getUserService,
            BookRepository bookRepository) {
        this.getUserService = getUserService;
        this.bookRepository = bookRepository;
    }

    public void borrow(long bookId, long userId){
        User user = getUserService.getById(userId);
        if(user.getBorrowedBooks().stream()
                .anyMatch(book -> book.getId()== bookId)){
            throw new IllegalStateException("User already borrowed " +
                    "the book");
        }
        if(user.getBorrowedBooks().size() >= 3){
            throw new IllegalStateException("User already has " +
                    "maximum number of books borrowed!");
        }
        Book book =
                bookRepository.findById(bookId)
                            .orElseThrow(() -> new EntityNotFoundException());

        if(book.getNumberOfInstances()-1 < 0){
            throw new IllegalStateException("There are no available" +
                    " books!");
        }
        book.getUsers().add(user);
        book.numberOfInstances(book.getNumberOfInstances()+1);
        bookRepository.save(book);
    }
    // Rest of the code is omitted
}

The @Service annotation transforms our class into Spring Bean controlled by ApplicationContext. The primary task of ApplicationContext is to control the lifecycles of each Spring Bean and provide them when they are needed.

After fetching the user that wants to borrow the book, we check if the user borrowed the book already. If that check passes, we can check our requirement of a maximum of three books at one point in time. Before allowing the user to borrow the book, we need to make sure that there is an instance of the book available.

Injecting the Repository Class

We use the dependency injection to obtain required classes into the GetBookService.class. For now, we only need the BookRepository.class. Dependency injection is explained in the previous chapter.

We can inject the GetBookRepository.class using three methods:

You can read a bit more about different types of dependency injection in this article.

Field-based Injection

Let us look at how to use the field-based injection:

@Service
public class GetBookService {
    @Autowired
    private final BookRepository bookRepository;

    // Rest of the code is omitted
}

Spring recognizes the @Autowired annotation and makes sure that the BookRepository is provided.

Setter-based Injection

@Service
public class GetBookService {

    private BookRepository bookRepository;

    @Autowired
    private void setBookRepository(BookRepository bookRepository){
        this.bookRepository = bookRepository;
    }

    // Rest of the code is omitted
}

We can put the @Autowired annotation on the setter method. The setter-based injection does not allow us to mark the variable as final.

Constructor-based Injection

@Service
public class GetBookService {

    private final BookRepository bookRepository;

    @Autowired
    public GetBookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    // Rest of the code is omitted
}

For the constructor-based annotation, we should set the @Autowired annotation on the constructor.

Learn about why you should usually choose constructor injection over the other types in this article.

Building a Web Controller

After creating entities, repositories, and services let us create our first controller that is solving the requirement that the application should be accessible through the browser.

Spring Boot High-level arch

The imported spring-boot-starter-web contains everything that we need for the controller. We have Spring MVC autoconfigured and the local server ready to use.

With Spring MVC, we can define a controller with the @Controller or @RestController annotation so it can handle incoming requests.

Creating an Endpoint With @RestController

We are going to create our first endpoint. We want to fetch all books that the bookstore owns. This is the endpoint that will be called from the homepage.

Let us look into the codebase:

@RestController
@RequestMapping("/books")
public class BooksRestController {

    private final GetBookService getBookService;

    @Autowired
    public BooksRestController(GetBookService getBookService) {
        this.getBookService = getBookService;
    }

    @GetMapping
    List<BookResponse> fetchAllBooks(){
        return getBookService.getAllBooks();
    }

    // Rest of the code 
}

To make our class the controller bean we need to annotate it with @RestController or with @Controller. The difference between these two is that @RestController automatically wraps the return object from the methods annotated with @GetMapping, @PostMapping, etc. into ResponseEntity<>.

We went with @RestController because it gives cleaner and more readable code.

The @RequestMapping("/books") annotation maps our bean to this path. If we start the application locally we can access the endpoint on the http://localhost:8080/books.

We annotated the fetchAllBooks() method with the @GetMapping annotation to define that this is the GET method. Since we didn’t define an additional path on the @GetMapping annotation we are using the path from the @RequestMapping definitions.

Creating an Endpoint With the @Controller Annotation

Instead with @RestController we can define the controller bean with the @Controller annotation:

@Controller
@RequestMapping("/controllerBooks")
public class BooksController {

    private final GetBookService getBookService;

    @Autowired
    public BooksController(GetBookService getBookService) {
        this.getBookService = getBookService;
    }

    @GetMapping
    ResponseEntity<List<BookResponse>> fetchAllBooks(){
        return ResponseEntity.ok(getBookService.getAllBooks());
    }
    // Rest of the code omitted

}

When using the @Controller annotation we need to make sure that we wrap the result in the ResponseEntity<> class.

This approach gives us more freedom when returning objects than @RestController. Let us imagine we are rewriting some legacy backend code of the Spring Boot project. One of requirements was that the current frontend will continue working throughout our refactoring. Previous code always returned the 200 code but the body would differ if some error occurred. In a scenario like this we can use the @Controller annotation and control the body and status code that is returned to the user.

Creating a POST Endpoint

The POST method is used when we want to create a new resource in the database.

With this POST method, we will cover the requirement “The admin user can add new book into the bookstore”.

Now, let us take a look how we can create new data:

@RestController
@RequestMapping("/admin/books")
public class AdminBooksRestController {
    
    private final CreateBookService createBookService;

    @Autowired
    public AdminBooksRestController(CreateBookService createBookService) {
        this.createBookService = createBookService;
    }

    @PostMapping
    BookResponse create(@RequestBody BookRequest request){
        return createBookService.createBook(request);
    }

    // Rest of the code omitted

}

@PostMapping defines that this is the POST method and that we want to create a new resource through it.

The @RequestBody annotation defines that we are expecting the data inside the HTTP requests body. That date should be serializable to the BookRequest instance.

Creating a PUT Endpoint

The PUT method is used when we want to update the resource that is already in the database. With this endpoint we are allowing the admin user to update information about a book:

@RestController
@RequestMapping("/admin/books")
public class AdminBooksRestController {
    
    private final CreateBookService createBookService;

    private final UpdateBookService updateBookService;

    private final DeleteBookService deleteBookService;

    @Autowired
    public AdminBooksRestController(CreateBookService createBookService,
                                    UpdateBookService updateBookService,
                                    DeleteBookService deleteBookService) {
        this.createBookService = createBookService;
        this.updateBookService = updateBookService;
        this.deleteBookService = deleteBookService;
    }


    @PutMapping("/{id}")
    BookResponse update(@PathVariable("id") long id,
                        @RequestBody BookRequest request){
        return updateBookService.updateBook(id,request);
    }

    // Rest of the code omitted
}

In the @PutMapping annotation, we define the path that continues on the one defined with @RequestMapping at the top of the class. The path looks like this: http://localhost:8080/admin/books/{id}.

The id variable is called the path variable and we can pass it into the method using the @PathVariable("id") annotation on the method argument. We need to be careful that the value inside @PathVariable matches the value inside @PutMapping.

The body of the method is defined with the @RequestBody annotation and we pass it as the json object inside the HTTP request.

Creating a DELETE Endpoint

With the DELETE endpoint we are meeting the requirement that admin users can delete a book from the bookstore:

@RestController
@RequestMapping("/admin/books")
public class AdminBooksRestController {
    

    private final CreateBookService createBookService;

    private final UpdateBookService updateBookService;

    private final DeleteBookService deleteBookService;

    @Autowired
    public AdminBooksRestController(CreateBookService createBookService,
                               UpdateBookService updateBookService,
                               DeleteBookService deleteBookService) {
        this.createBookService = createBookService;
        this.updateBookService = updateBookService;
        this.deleteBookService = deleteBookService;
    }

    @DeleteMapping("/{id}")
    void delete(@PathVariable("id") long id){
        deleteBookService.delete(id);
    }

    // Rest of the code omitted

}

With @DeleteMapping("/{id}") we define which resource we want to delete. We can see that the path is the same as in the PUT endpoint but the HTTP method is different. The paths have to be unique for the same HTTP method.

Calling an Endpoint

After we build our endpoints we want to test them and see what do we get as the result. Since we don’t have any frontend we can use command-line tools or a graphical tool like Postman. With the cURL command-line tool, we can do the following, for example:

curl --location --request GET 'http://localhost:8080/books'

We will get a result like the following:

[
    {
        "title": "The Sandman Vol. 1: Preludes & Nocturnes",
        "author": "Neil Gaiman",
        "publishedOn": "19/10/2010",
        "currentlyAvailableNumber": 4
    },
    {
        "title": "The Lord Of The Rings Illustrated Edition",
        "author": "J.R.R. Tolkien",
        "publishedOn": "16/11/2021",
        "currentlyAvailableNumber": 1
    }
]

Conclusion

After deciding which dependencies we needed and generating the project, we looked at how to create a functional application that can store and retrieve data from a database via a REST API.

We learned how to build the basic Spring Boot application, and went through several concepts:

Spring Boot provides all the scaffolding for us, and we can focus on building the business logic of our application.

You can browse the source code of the Spring Boot application on GitHub.

Written By:

Mateo Stjepanović

Written By:

Mateo Stjepanović

Fullstack developer focused on Spring and Angular. I started writing to continue my learning path and give something back to the dev community.

Recent Posts

Guide to JUnit 5 Functional Interfaces

In this article, we will get familiar with JUnit 5 functional interfaces. JUnit 5 significantly advanced from its predecessors. Features like functional interfaces can greatly simplify our work once we grasp their functionality.

Read more

Getting Started with Spring Security and JWT

Spring Security provides a comprehensive set of security features for Java applications, covering authentication, authorization, session management, and protection against common security threats such as CSRF (Cross-Site Request Forgery).

Read more

Creating and Publishing an NPM Package with Automated Versioning and Deployment

In this step-by-step guide, we’ll create, publish, and manage an NPM package using TypeScript for better code readability and scalability. We’ll write test cases with Jest and automate our NPM package versioning and publishing process using Changesets and GitHub Actions.

Read more