CRUD stands for (create, read, update and delete). Today I'm going to build a backend application using spring boot. I will use rest API, Spring Data JPA and Postgresql. This will be a monolith application. To achieve the CRUD behavior we mainly need the following HTTP methods.
POST: creating a resource.
GET: viewing a resource.
PUT: updating a resource.
DELETE: deleting a resource.
The application
Today I'm going to build a super simple Pastebin-type application. The features are
People can put their text or snippet in the application for a certain time. After that time, the snippet will be removed automatically.
For simplicity, I've kept only the public snippets.
The snippet size is a maximum of 2000.
User can put their name and the snippet's title. Both of those fields' size is a maximum of 100.
Overview of the application
The purpose of this article is to focus on the CRUD operations. That's why I've decided to keep this application minimal and simple. I will use the entity, repository, service and controller model. Here is the rest API design for our application.
URL | VERB | Description |
/pastes | POST | Create a paste |
/pastes?start=0&size=10 | GET | Get the first set of Pastes |
/pastes/7 | GET | Get a specific paste |
/pastes/7 | PUT | Update a specific paste |
/pastes/7 | DELETE | Delete a specific paste |
Usually, a spring boot application has 3 layers.
Controller layer: this part is responsible to handle incoming HTTP requests and produce responses.
Service layer: We should put the business logic and validations here. So, complex logic should have been placed here.
Persistence layer: This layer is responsible to interact with the database or other data sources. This layer is consisting of entities and repositories.
Initialize the project
Go to the Spring Initializer. Provide basic information. This part will also describe the technologies I've used. I've chosen
Project: Gradle - Groovy
Language: Java - 17
Spring boot version: 3.1.0
Group: com.leeonscoding
Artifact: notes
Name: notes
Package name: com.leeonscoding.notes
Packaging: Jar
Java version: 17
I need some dependencies for this Project. Here is the list I have chosen.
Spring Web
Spring Data JPA
PostgreSQL Driver
Lombok
Jakarta bean validator
Click the 'GENERATE' button and extract it. Put the folder in a suitable workspace.
Properties configurations
Add these properties to the application.yaml file
spring:
datasource:
url: jdbc:postgresql://localhost/paste
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
Entities
Now, start with entities. Entity classes represent the structure of the database tables. An entity object represents a record in the database table. The JPA implementation provider(in our case Hibernate) will generate tables using the entity classes. Although we should use the Database migration tool, for simplicity I'm using this ORM tool. I've put all entity classes under the 'domain' subpackage. Let's define them one by one. I've only one entity class which I called Paste.java.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Paste {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(length = 100)
private String author;
@Column(length = 100, nullable = false, updatable = false)
private String title;
@Column(length = 2000, nullable = false)
private String content;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime updatedAt;
@Column(nullable = false)
private LocalDateTime deletionDateTime;
}
If we run this application now, Hibernate will generate and execute the SQL query, which can be seen in the debugger console.
create table paste (
id bigserial not null,
author varchar(100),
content varchar(2000) not null,
created_at timestamp(6) not null,
deletion_date_time timestamp(6) not null,
title varchar(100) not null,
updated_at timestamp(6) not null,
primary key (id)
)
Repository
Now, define the repository interface. I've called it PasteRepository.java. The parent of the JpaRepositoy is the Repository interface. The Repository is the marker interface. We have to pass the types and entity information to this marker interface. At runtime, this interface will generate all the CRUD methods for the database. Also, we can define complex queries in various ways.
public interface PasteRepository extends JpaRepository<Paste, Long> {
@Query("SELECT p from Paste p WHERE p.deletionDateTime < CURRENT_TIMESTAMP")
List<Paste> findByDeletionDateTime();
}
This is similar to the old Dao pattern. The only difference is we were responsible to write the implementations. But now the hibernate takes care of the common CRUD implementations. The JpaRepository has the abstraction. It takes two parameters, the entity class and the primary key type. The JpaRepository extends from ListCrudRepository and ListPagingAndSortingRepository. So, the necessary basic implementations for CRUD, paging and sorting operations already exists. As this app is super simple, we can use those provided methods. This pattern separates from the complexity of implementations, we need to call the interface only. Also, if we need to modify anything, we just put our modification in the PasteRepository. Here, I've used a custom query in the @Query annotation.
Service
The purpose of the service layer is to work as a bridge between the repository and the controller layer. Let's use those in basic features in the Service classes. First, define the service interface(PasteService.java). Those method names can describe the purpose of their job.
public interface PasteService {
int DELETION_HOUR = 1;
int MAX_TITLE_LENGTH = 100;
int MAX_AUTHOR_LENGTH = 100;
int MAX_CONTENT_LENGTH = 2000;
void addPaste(PasteCreateInput paste);
Paste getOne(long id);
List<Paste> getAllByPage(int start, int size);
void updatePaste(long id, PasteUpdateInput paste);
void deletePaste(long id);
List<Paste> getOneByDeletionDateTime();
}
Now, the implementation class(PasteServiceImpl.java)
@Log4j2
@Service
public class PasteServiceImpl implements PasteService {
@Autowired
private PasteRepository pasteRepository;
@Override
public void addPaste(PasteCreateInput pasteInput) throws RuntimeException {
LocalDateTime now = LocalDateTime.now();
Paste temp = Paste.builder()
.author(pasteInput.author())
.title(pasteInput.title())
.content(pasteInput.content())
.createdAt(now)
.updatedAt(now)
.deletionDateTime(now.plusHours(PasteService.DELETION_HOUR))
.build();
Paste newPaste = pasteRepository.save(temp);
log.info("A paste is created" + newPaste.getId());
}
@Override
public Paste getOne(long id) {
return pasteRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Paste not found"));
}
@Override
public List<Paste> getAllByPage(int start, int size) throws RuntimeException {
Sort.TypedSort<Paste> noteSort = Sort.sort(Paste.class);
Sort sort = noteSort.by(Paste::getCreatedAt)
.descending();
Pageable pageable = PageRequest.of(start, size, sort);
return pasteRepository.findAll(pageable).toList();
}
@Override
public void updatePaste(long id, PasteUpdateInput pasteInput) throws RuntimeException {
Paste oldPaste = pasteRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Paste not found"));
if (!(pasteInput.author() == null || pasteInput.author().isBlank())) {
oldPaste.setAuthor(pasteInput.author());
}
if (!(pasteInput.content() == null || pasteInput.content().isBlank())) {
oldPaste.setContent(pasteInput.content());
}
oldPaste.setUpdatedAt(LocalDateTime.now());
pasteRepository.save(oldPaste);
log.info("A paste is updated: " + oldPaste.getId());
}
@Override
public void deletePaste(long id) throws RuntimeException {
pasteRepository.deleteById(id);
log.warn("A paste is deleted: " + id);
}
@Override
public List<Paste> getOneByDeletionDateTime() {
return pasteRepository.findByDeletionDateTime();
}
}
I've put most of the validations here. It's a good practice to put business logic in the service classes. Here in the getAllByPage() method, I've used the JPA paging and sorting techniques. I've used 2 different POJO classes(records) as inputs. This is similar to the DTO pattern. So, the user doesn't need to put in any unnecessary inputs as the entity 'Paste' class has more fields. Here are the record class details.
public record PasteCreateInput(@NotNull @Max(PasteService.MAX_CONTENT_LENGTH) String content,
@NotNull @Max(PasteService.MAX_TITLE_LENGTH) String title,
@NotNull @Max(PasteService.MAX_AUTHOR_LENGTH) String author) {
}
public record PasteUpdateInput(@Max(PasteService.MAX_CONTENT_LENGTH) String content,
@Max(PasteService.MAX_AUTHOR_LENGTH) String author) {
}
Those classes have some useful bean validations.
Exception handling
In my design, the Service class and also the Controller class can throw the Runtime exception. I've written an Exception handler class that intercepts those exceptions and provide a custom error message.
@ControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler
public ResponseEntity<ErrorOutput> badRequestParamException(RuntimeException e) {
return new ResponseEntity<>(
new ErrorOutput("Bad message format. Please give a valid input"),
HttpStatus.BAD_REQUEST);
}
}
Here, the @ControllerAdvice annotation can handle all the exceptions globally across the whole application. The ErrorOutput is a simple record that holds the error message.
public record ErrorOutput(String message) {
}
Controller
Finally, here is the Controller class PasteController.java which intercepts all HTTP requests and provides responses. It gets all the required data from the service class.
@RestController
@RequestMapping("/pastes")
public class PasteController {
@Autowired
private PasteService pasteService;
@PostMapping
public ResponseEntity<HttpStatus> addPaste(@RequestBody PasteCreateInput paste) {
pasteService.addPaste(paste);
return new ResponseEntity<>(HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<Paste> getSpecificPaste(@PathVariable long id) {
Paste paste = pasteService.getOne(id);
return new ResponseEntity<>(paste, HttpStatus.OK);
}
@GetMapping
public ResponseEntity<List<Paste>> getPages(@RequestParam int start,
@RequestParam int size) {
if (start < 0 || start > Integer.MAX_VALUE - 1) start = 0;
if (size < 10 || size > Integer.MAX_VALUE - 1) start = 10;
List<Paste> pastes = pasteService.getAllByPage(start, size);
return new ResponseEntity<>(pastes, HttpStatus.OK);
}
@PutMapping("/{id}")
public ResponseEntity<HttpStatus> updatePaste(@PathVariable long id,
@RequestBody PasteUpdateInput paste) {
pasteService.updatePaste(id, paste);
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<HttpStatus> deletePaste(@PathVariable long id) {
pasteService.deletePaste(id);
return new ResponseEntity<>(HttpStatus.OK);
}
}
Testing
Now, I've decided to test this particular application using Postman.
For a successful GET request.
For an unsuccessful POST request,
I've added the Postman exported file to the codebase.
You can find the full code in this GitHub link.
Conclusion
Developing a CRUD application in Spring Boot isn't a difficult thing. I've shared my thinking on how I've developed one. Please share your opinion on how was it in the comment section. If you like this article then give me a like.