Building a Restful CRUD API using Spring Boot, PostgreSQL, JPA, and Hibernate

Spring Boot offers a fast way to build applications and with the help of it the setup time required for projects and configuration has drastically reduced.

Building a Restful CRUD API using Spring Boot, PostgreSQL, JPA, and Hibernate

Spring Boot is an opinionated, easy to get-started addition to the Spring platform. It looks your classpath and at the beans you have configured, makes reasonable assumptions about what you are missing, adds those items.

You can setup a project with almost zero configuration and start building the things that matter to your application.

In this blog post, we will build a Restful CRUD API for notes application which simply has a title and description. It has features to create, update, delete, retrieve note and get all notes. To have these features, we need to create APIs.

So, let’s get started!

α. Creating the project:

Spring Boot provides Spring Initializr, a quick start generator for Spring Projects. Head over to https://start.spring.io to access this web tool. I have already filled required details for this project, you can use this link to generate the same.

Click Generate and a zip file will be downloaded which contains your project related settings and a pom.xml to get dependencies. You can unzip it and import it into your favourite IDE.

(OR)

Some IDEs such as IntelliJ, STS will allow you to create project right from it that has Spring Initializr. Once your setup is done, the project structure would look like this:

.gitignore
note-bytes
   |-- .gitignore
   |-- .mvn
   |   |-- wrapper
   |   |   |-- MavenWrapperDownloader.java
   |   |   |-- maven-wrapper.jar
   |   |   |-- maven-wrapper.properties
   |-- mvnw
   |-- mvnw.cmd
   |-- pom.xml
   |-- src
   |   |-- main
   |   |   |-- java
   |   |   |   |-- com
   |   |   |   |   |-- notebytes
   |   |   |   |   |   |-- NotebytesApplication.java
   |   |   |   |   |   |-- ServletInitializer.java
   |   |   |-- resources
   |   |   |   |-- application.properties
   |   |-- test
   |   |   |-- java
   |   |   |   |-- com
   |   |   |   |   |-- notebytes
   |   |   |   |   |   |-- NotebytesApplicationTests.java
pom.xml

Let’s try to understand details of some of the important files and directories.

1. NotebytesApplication.java

This file is the main entry point for our application to start, and it has main() method.

It contains a single annotation @SpringBootApplication. This annotation is used to mark a configuration class that declares one or more @Bean methods and also triggers auto-configuration and component scanning. This annotation is combination of @SpringBootConfiguration (@Configuration), @EnableAutoConfiguration, and @ComponentScan.

  • @Configuration: Any class annotated with @Configuration annotation is bootstrapped by Spring and it tags the class as a source of bean definitions for the application context.
  • @EnableAutoConfiguration: This annotation tells Spring Boot to start adding beans based on classpath settings, other beans, and various property settings. For example, if spring-webmvc is on the classpath, this annotation flags the application as a web application and activates key behaviours, such as setting up a DispatcherServlet. If spring-data-jpa or spring-jdbc is in the classpath, then it automatically tries to configure DataSource by reading database properties from application.properties file.
  • @ComponentScan: This annotation tells Spring to look for other components, configurations, and services in the current package (com.notebytes) and all sub-packages, letting it find the controllers.

2. resources/

This directory is dedicated to all static resources, templates and property files.

resources/static: contains static resources such as css, js, and images. But we won’t touch this folder.

resources/templates: contains server-side templates which are rendered by Spring. We won’t touch this folder also.

resources/application.properties. This is very important file and contains application-wide properties. Spring reads the properties defined in this file to configure your application. This is where we define our database related properties. This page has list of common properties used in Spring Boot.

3. NotebyteApplicationTests

Here we write unit and integration tests, but we won’t write any tests for this blog post.

4. pom.xml

This xml file is important and contains all the project dependencies.

β. Configuring PostgreSQL Database:

As I mentioned earlier, Spring Boot tries to auto-configure a DataSource if spring-data-jpa is in the classpath by reading database configuration from application.properties file.

So, our database configuration must be added. Let’s add it and Spring Boot will take care of the rest.

Open application.properties and add the properties from application.properties file which is attached to this blog post. You don’t have to worry for all the properties mentioned in it. For now, just focus on these below properties.

#---Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)---#
spring.datasource.url=jdbc:postgresql://localhost:5432/notebytes?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=password

#--Initialize the datasource with available DDL and DML scripts--#
#--spring.datasource.initialization-mode=always (deprecated), use below
spring.sql.init.mode=always

#--The SQL dialect makes Hibernate generate better SQL for the chosen database--#
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

#--Hibernate Logging--#
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE

####--Jackson Properties--####
# To disable serializing, Java 8 Data/Time values as timestamps.
# All the Date/Time values will be serialized to ISO date/time string.
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
spring.jackson.time-zone=UTC

You will need to create a database named notebytes in PostgreSQL and change the spring.datasource.username and spring.datasource.password as per your PostgreSQL installation.

Spring Boot uses Hibernate as the default JPA implementation. The property spring.jpa.hibernate.ddl-auto is used for database initialization. It has 5 values: create, create-drop, none, validate, and update. Here I have used create-drop, so the tables would be dropped and created again. You can provide update. When you provide update, it does two things:

  • When you define a domain model, a table will be created automatically in the database and the fields of domain model will be mapped to the corresponding columns in the table.
  • When you update or make changes in a domain model, it will trigger an update to the table. For example, when you add new field or change an existing one, all these changes would be reflected in the mapped table as well.

Using create-drop or update for spring.jpa.hibernate.ddl-auto property is fine for development. But, for production, you should keep the value of this property to "validate", and use a database migration tool like Flyway for managing changes in the database schema.

γ. Creating Note model:

Our Note model has following fields. createdAt and updatedAt will be defined in DateAudit class and our Note model will extend it.

  • id: Primary key with auto increment
  • title: The title of the Note (not null field)
  • description: Note content (not null field)
  • createdAt: The instant at which Note was created
  • updatedAt: The instant at which Note was updated

Let’s create this model in our project. Create a new package named model inside base package com.notebytes and create file named Note.java with the following content in it.

package com.notebytes.model;

import com.notebytes.model.audit.DateAudit;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;

@Getter
@Setter
@Entity
@Table(name = "notes")
public class Note extends DateAudit {

    @Id
    @Column(name = "note_id")
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notes_seq")
    @SequenceGenerator(name = "notes_seq", allocationSize = 1)
    private Long id;

    @NonNull
    @NotBlank
    private String title;

    @NonNull
    @NotBlank
    private String description;

}
  • All your domain models must be annotated with @Entity annotation. It is used to mark the class as persistence Java class.
  • @Table annotation is used to provide details of the table that this entity will be mapped to.
  • @Id annotation is used to define Primary Key.
  • @GeneratedValue annotation is used to define the primary key generation strategy. The JPA specification supports 4 different primary key generation strategies which generate the primary key values programmatically or use database features, like auto-incremented columns or sequences. The only thing you have to do is to add the @GeneratedValue annotation to your primary key attribute and choose a generation strategy.
    • GenerationType.AUTO: is the default generation type and lets the persistence provider choose the generation strategy. If you use Hibernate as your persistence provider, it selects a generation strategy based on the database specific dialect. For most popular databases, it selects GenerationType.SEQUENCE
      • @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    • GenerationType.IDENTITY: is the easiest to use but not the best one from a performance point of view. It relies on an auto-incremented database column and lets the database generate a new value with each insert operation. From a database point of view, this is very efficient because the auto-increment columns are highly optimized, and it doesn’t require any additional statements. This approach has a significant drawback if you use Hibernate. Hibernate requires a primary key value for each managed entity and therefore has to perform the insert statement immediately. This prevents it from using different optimization techniques like JDBC batching.
      • @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    • GenerationType.SEQUENCE: is my preferred way to generate primary key values and uses a database sequence to generate unique values. It requires additional select statements to get the next value from a database sequence. But this has no performance impact for most applications. And if your application has to persist a huge number of new entities, you can use some Hibernate specific optimizations to reduce the number of statements. If you don’t provide any additional information, Hibernate will request the next value from its default sequence. You can change that by referencing the name of a @SequenceGenerator in the generator attribute of the @GeneratedValue annotation. The @SequenceGenerator annotation lets you define the name of the generator, the name, and schema of the database sequence and the allocation size of the sequence.
      • @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE)
        private Long id;
    • GenerationType.TABLE: Rarely used nowadays. It simulates a sequence by storing and updating its current value in a database table which requires the use of pessimistic locks which put all transactions into a sequential order. This slows down your application, and you should, therefore, prefer the GenerationType.SEQUENCE, if your database supports sequences, which most popular databases do.
      • @Id
        @GeneratedValue(strategy = GenerationType.TABLE)
        private Long id;
  • @NotBlank annotation is used to validate that the annotated field is not null or empty.
  • @Column annotation is used to define the properties of the column that will be mapped to the annotated field.
  • I have used Lombok for getters and setters. You can read more about it here.
  • We have extended DateAudit class which we will write it next.

δ. Create DateAudit class:

Create package named audit inside model package and create file named DateAudit.java and add following content to it.

package com.notebytes.model.audit;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.time.Instant;

@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(
        value = {"createdAt", "updatedAt"},
        allowGetters = true
)
public abstract class DateAudit implements Serializable {

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private Instant updatedAt;
}

a) In the above class, we have annotated createdAt and updatedAt fields with @CreatedDate and @LastModifiedDate annotations respectively. Now, we want these fields to be populated automatically whenever an entry is created or updated.

To achieve this, there are two things to be done:

  1. Add Spring Data JPA’s AuditingEntityListener.
    • To do this add @EntityListeners(AuditingEntityListener.class) at the top of the class.
  2. Enable JPA Auditing in our application.
    • Create a package named config and inside base package and create a file named AuditConfig.java and add the following code to it. Make sure the class is annotated with @Configuration and @EnableJpaAuditing. [OR] You can add @EnableJpaAuditing annotation in your NotebytesApplication.java. But keeping it in separate file makes it cleaner and can be updated later for other purposes.
package com.notebytes.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class AuditConfig {

}

b) @JsonIgnoreProperties annotation is Jackson annotation. Spring Boot uses Jackson for Serializing and Deserializing Java objects to and from JSON.

This annotation is used because we don’t want the clients of the REST API to supply the createdAt and updatedAt values. If they supply these values, then we will ignore them. However, we include these values in JSON response.

c) @MappedSuperClass - Using this annotation, we can allow an entity to inherit properties from a super class. For example, in a typical application, we maintain below properties in every table.

  1. createdBy
  2. updatedBy
  3. createdAt
  4. updatedAt

Instead of putting all these properties in every entity, we can place these properties in a base entity (annotate base entity with MappedSuperClass annotation).

ε. Create NoteByteRepository to access data from the database:

We have created domain model, now it’s time to create notes, update, read and delete them. So, we need to create a repository. Spring Data JPA has got us covered here. It comes with JpaRepository interface which defines methods for all the CRUD operations on the entity, and a default implementation of JpaRepository is SimpleJpaRepository.

Create a package named repository inside the base package com.notebytes and create an interface named NoteBytesRepository.java and extend it from JpaRepository.

package com.notebytes.repository;

import com.notebytes.model.Note;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NoteBytesRepository extends JpaRepository<Note, Long> {

}

We have annotated the interface with @Repository annotation. This annotation is a marker for any class/interface that fulfils the role or stereotype of a repository (also known as Data Access Object or DAO). It is a stereotype for persistence layer. It tells Spring to bootstrap the repository during component-scan.

Annotation Meaning
@Component generic stereotype for any Spring-managed component
@Repository stereotype for persistence layer
@Service stereotype for service layer
@Controller stereotype for presentation layer (spring-mvc)

That’s it in the repository layer. Now we would be having methods like findAll(), save(), findById(), count(), delete() etc.

You don’t need to implement these methods. They are already implemented by Spring Data JPA’s SimpleJpaRepository. This implementation is plugged in by Spring automatically at runtime.

Spring Data JPA has a bunch of other interesting features like Query methods (dynamically creating queries based on method names), Criteria API, Specifications, QueryDsl etc.

ζ. Creating custom exception:

We’ll define the Rest APIs for creating, retrieving, updating, and deleting a Note in the next section.

The APIs will throw a ResourceNotFoundException whenever a Note with a given id is not found in the database. Create a package named exception in base package com.notebytes and create ResourceNotFoundException.java file with the following content.

package com.notebytes.exception;

import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@Getter
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {

    private final String resourceName;
    private final String fieldName;
    private final Object fieldValue;

    public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s: '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }
}

Notice the use of @ResponseStatus annotation in the above exception class. This will cause Spring boot to respond with the specified HTTP status code whenever this exception is thrown from your controller.

η. Creating NoteByteController:

The final step is to create REST APIs for create, update, retrieve and delete. Create a package named controller inside base package com.notebytes and create NoteBytesController.java file and add the following content to it.

package com.notebytes.controller;

import com.notebytes.repository.NoteBytesRepository;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RestController
@RequestMapping("/api/note-bytes")
public class NoteBytesController {

    private final NoteBytesRepository noteBytesRepository;

    @Autowired
    public NoteBytesController(NoteBytesRepository noteBytesRepository) {
        this.noteBytesRepository = noteBytesRepository;
    }

    // Get all notes

    // Create a new note

    // Get a single note

    // Update a note
    
    // Delete a note
}

@RestController annotation is a combination of Spring’s @Controller and @ResponseBody annotations.

The @Controller annotation is used to define a controller and the @ResponseBody annotation is used to indicate that the return value of a method should be used as the response body of the request.

@RequestMapping("/api/note-bytes") declares that the url for all the apis in this controller will start with /api/note-bytes.

@Autowired annotation allows Spring to resolve and inject collaborating beans into our bean. We used auotowiring on constructor.

Now, we will go through the creation of APIs one-by-one.

1. Get all notes (GET /api/note-bytes/notes/all)

// Get all notes
@GetMapping("/notes/all")
public List<Note> getAllNoteBytes() {
    return noteBytesRepository.findAll();
}

The above method is straightforward. It calls JpaRepository’s findAll() method to retrieve all notes from the database and returns the entire list. The return type of findAll() is List<T>.

@GetMapping(“/notes/all”) is short form of @RequestMapping(value = “/notes/all”, method = RequestMethod.GET).

2. Create a new note (POST /api/notes-bytes/note/create)

// Create a new note
@PostMapping("/note/create")
public Note createNoteByte(@Valid @RequestBody Note note) {
    return noteBytesRepository.save(note);
}

The @RequestBody annotation is used to bind the request body with a method parameter. The @Valid annotation makes sure that the request body is valid. Remember, we had marked Note’s title and content with @NotBlank annotation in the Note model?

If the request body doesn’t have a title or a content, then spring will return a 400 BadRequest error to the client.

3. Get a single note (GET /api/note-bytes/notes/{id})

// Get a single note
@GetMapping("/notes/{id}")
public Note getNoteByteById(@PathVariable("id") final Long noteId) {
	return noteBytesRepository.findById(noteId)
			.orElseThrow(() -> new ResourceNotFoundException("Note", "note_id", noteId));
}

The @PathVariable annotation, as the name suggests, is used to bind a path variable with a method parameter. In the above method, we are throwing a ResourceNotFoundException whenever a Note with the given noteId is not found. This will cause Spring Boot to return a 404 Not Found error to the client (Remember, we had added a @ResponseStatus(value = HttpStatus.NOT_FOUND) annotation to the ResourceNotFoundException class).

4. Update a note (PUT /api/note-bytes/notes/{id})

// Update a note
@PutMapping("/notes/{id}")
public Note updateNoteByteById(@PathVariable("id") final Long noteId, @Valid @RequestBody Note noteDetails) {
	Note note = noteBytesRepository.findById(noteId)
			.orElseThrow(() -> new ResourceNotFoundException("Note", "note_id", noteId));
	note.setTitle(noteDetails.getTitle());
	note.setDescription(noteDetails.getDescription());

	return noteBytesRepository.save(note);
}

5. Delete a note (DELETE /api/note-bytes/notes/{id})

// Delete a note
@DeleteMapping("/notes/{id}")
public ResponseEntity<?> deleteNoteByte(@PathVariable("id") final Long noteId) {
	Note note = noteBytesRepository.findById(noteId)
			.orElseThrow(() -> new ResourceNotFoundException("Note", "note_id", noteId));
	noteBytesRepository.delete(note);
	return ResponseEntity.ok().build();
}

θ. Running the application:

We have successfully created APIs, now it’s time to test them. Run the application and open postman. The application will start and runs at port 8080 by default.

ι. Testing APIs:

a) Creating a note

b) Get all notes

c) Get a single note

d) Update a note

e) Delete a note

The complete source code for this project was added in Github and also attached to this blog post. You can check the github repo here.

We successfully built a Restful CRUD API using Spring Boot, PostgreSQL, JPA and Hibernate.

Files