IntelliJ IDEA Java

How to Build a CRUD REST API Using Spring Boot

Spring Boot is an application framework for the Java platform designed to make it easy to build Spring-powered applications. Its opinionated convention-over-configuration approach to building Spring applications improves developer productivity.

IntelliJ IDEA provides extensive coding assistance for Spring, including a dedicated project wizard, code highlighting, intelligent context actions, embedded documentation, navigation, and highly customizable run configurations. For a detailed description of the supported features, refer to IntelliJ IDEA for Spring Developers.

We are going to build a REST API to manage bookmarks using Spring Boot and PostgreSQL database by implementing CRUD (Create, Read, Update, Delete) API endpoints.

In this article, we are going to cover the following:

  • Create a Spring Boot project from IntelliJ IDEA
  • Run a PostgreSQL database instance
  • Connect to the database using IntelliJ IDEA’s database tools support
  • Database setup using an SQL script
  • Generate entities from the database
  • Create a Spring Data JPA repository
  • Add custom finder methods to the repository
  • Using Spring Data JPA projection
  • Invoke the repository methods using the JPA console
  • Implement API endpoint handler methods
  • Test API endpoints using the HTTP Client

While building the CRUD REST API using Spring Boot, we are going to use various features offered by IntelliJ IDEA Ultimate. If you haven’t already installed it, please download and install IntelliJ IDEA Ultimate.

Download IntelliJ IDEA

If you are using IntelliJ IDEA Community Edition, you can manually create the classes using the code snippets in this article.

Create a Spring Boot project

Let’s start with creating a new Spring Boot project. When you open the IntelliJ IDEA New Project wizard, you can see there is dedicated support for creating Spring Boot projects.

Select Java as the language and provide values for Group, Artifact, and Package name. We are going to use Gradle – Groovy as the build tool in this article, but you can choose other alternatives if you prefer. We are going to use Java 21 and Jar packaging.

Click Next, select the following dependencies, and then click the Create button.

  • Spring Web
  • Validation
  • Spring Data JPA
  • PostgreSQL Driver

Now, the project will be created and opened in the IDE.

Run a PostgreSQL database instance

We need a PostgreSQL database instance for running our application. You can download and install the PostgreSQL database from https://www.postgresql.org/download/. If you have Docker installed, you can use Docker to run a PostgreSQL database container using the following command:

docker run -p 5432:5432 \
 -e POSTGRES_PASSWORD=postgres \
 -e POSTGRES_USER=postgres \
 -e POSTGRES_DB=postgres \
 postgres:17

The above command will pull the Docker image postgres:17 from Docker Hub, if not already pulled, start a Postgres container, and map container port 5432 to host port 5432. The username, password, and database values are passed using environment variables. You can check whether Postgres is running by running the docker ps command as follows:

Connect to PostgreSQL using IntelliJ IDEA’s database tools support

Now that the PostgreSQL database is running, let’s connect to it using IntelliJ IDEA’s database tools support and the following connection parameters:

Host: localhost
Port: 5432
Username: postgres
Password: postgres
Database: postgres

You can open the Database tool window either by clicking on the database icon on the right-hand side toolbar or by going to View | Tool Windows | Database.

Create a new data source of type PostgreSQL by providing the following information:

You can click on Test Connection to verify whether the database connection can be established successfully or not.

Database setup using an SQL script

Select the PostgreSQL datasource from the Database tool window and open the query console. Now, let’s create a database table called bookmarks and then populate some sample data by running the following SQL script:

create sequence bookmark_id_seq start with 1 increment by 50;

create table bookmarks
(
    id         bigint       not null default nextval('bookmark_id_seq'),
    title      varchar(200) not null,
    url        varchar(500) not null,
    created_at timestamp    not null default now(),
    updated_at timestamp,
    primary key (id)
);

insert into bookmarks(title, url, created_at) values
('IntelliJ IDEA documentation', 'https://www.jetbrains.com/help/idea/getting-started.html', '2021-06-26'),
('IntelliJ IDEA YouTube channel', 'https://www.youtube.com/intellijidea', '2021-10-10'),
('JetBrains Guide', 'https://www.jetbrains.com/guide/', '2023-12-05'),
('Java Guide', 'https://www.jetbrains.com/guide/java/', '2024-08-15');

Now, let’s configure the database connection properties in the src/main/resources/application.properties file as follows:

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres

When we start our Spring Boot application, a database connection pool will be created connecting to the configured Postgres database.

Generate entities from the database

JPA (Jakarta Persistence API) is a standard Jakarta EE API for ORM (Object Relational Mapping) frameworks. JPA allows you to map the relational data model with an object model, providing an object-oriented way to work with databases.

IntelliJ IDEA Ultimate provides support for working with JPA, code assistance, etc. From IntelliJ IDEA, we can generate JPA entities from an existing database schema.

IntelliJ IDEA provides Reverse Engineering capabilities for generating JPA entities from an existing database out of the box. However, we will use the JPA Buddy plugin, which provides a lot more features that make working with JPA and Spring Data JPA much easier.

Go to Settings | Plugins | Marketplace, search for “jpa buddy”, and install the JPA Buddy plugin.

Now, you can generate JPA entities from a database by right-clicking on a Java package in the Project tool window and selecting New | JPA Entities from DB from the context menu.

Now, you will have a Bookmark JPA entity generated as follows:

package com.jetbrains.bookmarks;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;

@Entity
@Table(name = "bookmarks")
public class Bookmark {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "bookmarks_id_gen")
    @SequenceGenerator(name = "bookmarks_id_gen", sequenceName = "bookmark_id_seq")
    @Column(name = "id", nullable = false)
    private Long id;

    @Size(max = 200)
    @NotNull
    @Column(name = "title", nullable = false, length = 200)
    private String title;

    @Size(max = 500)
    @NotNull
    @Column(name = "url", nullable = false, length = 500)
    private String url;

    @NotNull
    @ColumnDefault("now()")
    @Column(name = "created_at", nullable = false)
    private Instant createdAt;

    @Column(name = "updated_at")
    private Instant updatedAt;

    // setters & getters
}

Create Spring Data JPA repository

The JPA Buddy plugin also makes it easy to create a Spring Data JPA repository for a JPA entity.

Right-click on a Java package and then select New | Spring Data JPA Repository.

Select Bookmark as Entity and click OK.

The BookmarkRepository will be created as follows:

package com.jetbrains.bookmarks;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
}

To get all the bookmarks in reverse chronological order of their creation, we can use the inherited findAll(Sort sort) method as follows:

Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
List<Bookmark> bookmarks = bookmarkRepository.findAll(sort);

Alternatively, we can also leverage Spring Data JPA’s derived query method feature and create the following method to achieve the same:

public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
    List<Bookmark> findAllByOrderByCreatedAtDesc();
}

With the findAllByOrderByCreatedAtDesc() method name, Spring Data JPA automatically prepares the SQL query that uses the order by created_at desc clause.

While this works fine, it is not recommended to return JPA entities as HTTP API responses. Instead, we can return Spring Data JPA interface-based projections.

Assuming we just want to return only the id, title, url, and createdAt fields of Bookmark, we can create a Spring Data JPA projection using the JPA Buddy plugin as follows:

Right-click on Bookmark.java and select New | Other | Spring Data Projection.

Now, you can change the Projection class name if needed, select the desired fields, and then click OK.

The BookmarkInfo.java interface will be generated as follows:

package com.jetbrains.bookmarks;

import java.time.Instant;

/**
 * Projection for {@link Bookmark}
 */
public interface BookmarkInfo {
    Long getId();

    String getTitle();

    String getUrl();

    Instant getCreatedAt();
}

Now, let’s update the BookmarkRepository to return the projection as follows:

public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
    List<BookmarkInfo> findAllByOrderByCreatedAtDesc();
}

Let’s also add a method to find a bookmark for the given id as follows:

package com.jetbrains.bookmarks;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
    List<BookmarkInfo> findAllByOrderByCreatedAtDesc();

    Optional<BookmarkInfo> findBookmarkById(Long id);
}

We are returning Optional<BookmarkInfo> because a bookmark with the given id may or may not exist.

IntelliJ IDEA provides the ability to invoke Spring Data JPA repository methods using the JPA console. So, we can call the repository methods and check whether they are returning the expected result or not.

Click on the gutter icon to run the query in the JPA console:

If you are using Gradle, you’ll need to select a persistence unit from the ones available. Select the bookmarks.main Persistence Unit.

Now, provide inputs to the method if required, and then you’ll see the results as follows:

NOTE: In a typical three-layered architecture, there will be the Web, Service, and Persistence layers. In the Service layer, we usually implement business logic as a unit of work, which might involve multiple database operations, as a transactional operation. In our sample application, as we don’t have any complex business logic, the controller will directly talk to the repository.

Implement Controller API handler methods

Let’s implement the API endpoint handlers for performing CRUD operations.

First, let’s create a BookmarkNotFoundException, which we will throw when the requested bookmark is not found.

package com.jetbrains.bookmarks;

public class BookmarkNotFoundException extends RuntimeException {
    public BookmarkNotFoundException(String message) {
        super(message);
    }
}

Now, let’s create BookmarkController as follows:

package com.jetbrains.bookmarks;

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

@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController {
    private final BookmarkRepository bookmarkRepository;

    BookmarkController(BookmarkRepository bookmarkRepository) {
        this.bookmarkRepository = bookmarkRepository;
    }

    //CRUD API handler methods
}

TIP: With IntelliJ IDEA, you don’t need to manually define and inject the dependencies before using them. When you need to use another Spring bean, you can start typing the dependent bean name, and IntelliJ IDEA will show the matching beans. When you select a bean, IntelliJ IDEA will automatically define and autowire the dependent bean using a constructor.

Let’s implement API handler methods to get all bookmarks (GET /api/bookmarks) and get bookmark by id( GET /api/bookmarks/{id}) as follows:

package com.jetbrains.bookmarks;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/bookmarks")
public class BookmarkController {
    //...

    @GetMapping
    List<BookmarkInfo> getBookmarks() {
        return bookmarkRepository.findAllByOrderByCreatedAtDesc();
    }

    @GetMapping("/{id}")
    ResponseEntity<BookmarkInfo> getBookmarkById(@PathVariable Long id) {
        var bookmark = 
             bookmarkRepository.findBookmarkById(id)
             .orElseThrow(()-> new BookmarkNotFoundException("Bookmark not found"));
        return ResponseEntity.ok(bookmark);
    }
}

NOTE: For the GET /api/bookmarks API, we are returning all the bookmarks, but in a real-world application, we would recommend using pagination.

Next, let’s implement the create bookmark (POST /api/bookmarks) handler method.

package com.jetbrains.bookmarks;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.time.Instant;
import java.util.List;

@RestController
@RequestMapping("/api/bookmarks")
public class BookmarkController {
    //...
    record CreateBookmarkPayload(
            @NotEmpty(message = "Title is required")
            String title,
            @NotEmpty(message = "Url is required")
            String url) {}

    @PostMapping
    ResponseEntity<Void> createBookmark(
            @Valid @RequestBody CreateBookmarkPayload payload) {
        var bookmark = new Bookmark();
        bookmark.setTitle(payload.title());
        bookmark.setUrl(payload.url());
        bookmark.setCreatedAt(Instant.now());
        var savedBookmark = bookmarkRepository.save(bookmark);
        var url = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}")
                .build(savedBookmark.getId());
        return ResponseEntity.created(url).build();
    }
}

We have created a local record CreateBookmarkPayload to represent the request payload and used the Jakarta Bean Validation API to validate the required fields. If the request payload is valid, then we create the Bookmark object and persist it in the database. Then, we construct the URL for the newly created bookmark and send it as the Location header. If the request payload is invalid, then by default, Spring Boot will handle this error by returning the HTTP status code 400 - BAD_REQUEST with the default error response.

Next, let’s implement Update (PUT /api/bookmarks/{id}) and delete the bookmark (DELETE /api/bookmarks/{id}) handler methods.

package com.jetbrains.bookmarks;
//imports

@RestController
@RequestMapping("/api/bookmarks")
public class BookmarkController {
    //...
    record UpdateBookmarkPayload(
            @NotEmpty(message = "Title is required")
            String title,
            @NotEmpty(message = "Url is required")
            String url) {
    }

    @PutMapping("/{id}")
    ResponseEntity<Void> updateBookmark(
            @PathVariable Long id,
            @Valid @RequestBody UpdateBookmarkPayload payload) {
        var bookmark =
              bookmarkRepository.findById(id)
                .orElseThrow(()-> new BookmarkNotFoundException("Bookmark not found"));
        bookmark.setTitle(payload.title());
        bookmark.setUrl(payload.url());
        bookmark.setUpdatedAt(Instant.now());
        bookmarkRepository.save(bookmark);
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping("/{id}")
    void deleteBookmark(@PathVariable Long id) {
        var bookmark =
             bookmarkRepository.findById(id)
             .orElseThrow(()-> new BookmarkNotFoundException("Bookmark not found"));
        bookmarkRepository.delete(bookmark);
    }
}

We have created a local record UpdateBookmarkPayload to represent the update request payload and used the Jakarta Bean Validation API to validate the required fields. If the request payload is valid, then we try to load the Bookmark object from the database. If the bookmark doesn’t exist, the code will throw a BookmarkNotFoundException. Otherwise, we will update the bookmark data in the database.

Similarly, for the delete bookmark API endpoint, we try to delete the bookmark if it exists. Otherwise, a BookmarkNotFoundException is thrown.

When we throw a BookmarkNotFoundException from our handler methods, by default, Spring is going to handle it and return a response with the HTTP status code 500 - INTERNAL_SERVER_ERROR. But when the bookmark is not found, it makes sense to send the 404 - NOT_FOUND status code. So, let’s implement an ExceptionHandler as follows:

package com.jetbrains.bookmarks;
//imports

@RestController
@RequestMapping("/api/bookmarks")
public class BookmarkController {
    //...
    
    @ExceptionHandler(BookmarkNotFoundException.class)
    ResponseEntity<Void> handle(BookmarkNotFoundException e) {
        return ResponseEntity.notFound().build();
    }
}

Now, when any handler method in BookmarkController throws a BookmarkNotFoundException, it will be handled by the ExceptionHandler and return the HTTP status code 404 - NOT_FOUND.

When you create a Spring Boot application in IntelliJ IDEA, it automatically creates a Spring Boot run configuration. Now run/debug the application using the BookmarksApplication run configuration, which should start the application on port 8080.

Test API endpoints using the HTTP Client

IntelliJ IDEA provides a built-in HTTP Client, which we can use to invoke our API endpoints and assert the responses.

You can open the HTTP Client by selecting Tools | HTTP Client | Create request in HTTP Client. Alternatively, you can also click on the API gutter icon on the API handler methods in BookmarkController.

IntelliJ IDEA will open a .http file. Once the application is started, you can invoke the GET /api/bookmarks endpoint and assert the response status code as follows:

### Get all bookmarks
GET http://localhost:8080/api/bookmarks

> {%
client.test("Should get bookmarks successfully", function () {
client.assert(response.status === 200, "Response status is not 200");
});
%}

You can click on the Run icon in the gutter to execute the API call.

Similarly, you can invoke other API endpoints as follows:

### Get bookmark by id
GET http://localhost:8080/api/bookmarks/1

> {%
client.test("Should get bookmark successfully", function () {
client.assert(response.status === 200, "Response status is not 200");
});
%}


### Create bookmark successfully
POST http://localhost:8080/api/bookmarks
Content-Type: application/json

{
  "title": "bookmark title",
  "url": "https://bookmark.com"
}

> {%
client.test("Should create bookmark successfully", function () {
client.assert(response.status === 201, "Response status is not 201");
});
%}

I will leave the rest of the API call invocations as an exercise for you. To learn more about the HTTP Client, you can refer to the documentation or watch The New HTTP Client presentation.

Summary

In this article, we learned how to create a REST API performing CRUD operations using Spring Boot talking to a PostgreSQL database. We have verified whether the application is working as expected or not using the HTTP Client.

You can find the complete code in this GitHub repository.

The bookmarks application built in this article can also be implemented using Kotlin and IntelliJ IDEA offers the same features for Kotlin as well. You can checkout the Kotlin based implementation of the bookmarks application in kotlin branch of the repository.

As we have seen, IntelliJ IDEA Ultimate supports Spring in a way that makes it easy to build Spring Boot applications. Also, the JPA Buddy plugin makes developers more productive while using JPA and Spring Data JPA-based applications.

Download IntelliJ IDEA

To learn more about JPA Buddy, you can watch this presentation or read the official documentation.

image description