Simplify CRUD with a Generic Controller to Reduce Boilerplate Code in Spring Boot

Mayank Yaduvanshi
3 min readMar 6, 2024

--

Photo by Glen Carrie on Unsplash

Introduction

In Spring Boot applications, creating RESTful APIs to perform CRUD (Create, Read, Update, Delete) operations is a common requirement. To reduce boilerplate code and improve code reusability, we can create a generic CRUD controller that can be extended by other resource controllers. In this blog post, we will explore how to create such a generic CRUD controller in Spring Boot.

Source Code

See this Git repository for full code examples. Github link

https://github.com/mayank042/spring-samples-generic-controller

Step 1: Define a Generic CRUD Service Interface

First, we define a generic interface for our CRUD service. This interface will define methods for performing CRUD operations on entities. Here is an example:

public interface CrudService<T, ID> {
Iterable<T> getAll();
Optional<T> getById(ID id);
T create(T entity);
T update(ID id, T entity);
void delete(ID id);
}

Step 2: Implement the Generic CRUD Service

Next, we implement the CrudService interface in a generic service class. This class will contain the actual implementation of the CRUD operations using a CrudRepository. Here is an example:

import org.springframework.data.repository.CrudRepository;
import java.util.Optional;

public abstract class CrudServiceImpl<T, ID> implements CrudService<T, ID> {

private final CrudRepository<T, ID> repository;

public CrudServiceImpl(CrudRepository<T, ID> repository) {
this.repository = repository;
}

// Implement CRUD methods
// See the source code for full implementation, link at the top
}

Step 3: Create a Custom Exception Class

We create a custom exception class to handle resource not found exceptions. This will be useful when an entity is not found during update or delete operations. Here is an example:

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

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

public ResourceNotFoundException(String message) {
super(message);
}
}

Step 4: Implement a Specific Service

Now, we implement a specific service for our resource, extending the CrudServiceImpl class. This class will provide the CRUD operations for a specific entity type. Here is an example:

import org.springframework.stereotype.Service;

@Service
public class ProductService extends CrudServiceImpl<Product, Long> {

public ProductService(ProductRepository repository) {
super(repository);
}
}

Step 5: Create a generic controller with CRUD endpoints

Now, let’s create a generic controller that will handle CRUD operations for any entity type. This controller will use the CrudService interface we defined earlier to perform CRUD operations. By creating a generic controller, we can avoid duplicating code for each entity and ensure consistent behavior across different resources.

Here’s an example of how the generic controller can be implemented:

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

import java.util.Optional;

public abstract class CrudController<T, ID> {

private final CrudService<T, ID> service;

protected CrudController(CrudService<T, ID> service) {
this.service = service;
}

@GetMapping("/all")
public Iterable<T> getAll() {
return service.getAll();
}

@GetMapping("/{id}")
public ResponseEntity<T> getById(@PathVariable ID id) {
Optional<T> optionalEntity = service.getById(id);
return optionalEntity.map(entity -> new ResponseEntity<>(entity, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
}

@PostMapping
public T create(@RequestBody T entity) {
return service.create(entity);
}

@PutMapping("/{id}")
public ResponseEntity<T> update(@PathVariable ID id, @RequestBody T entity) {
service.update(id, entity);
return new ResponseEntity<>(entity, HttpStatus.OK);
}

@PatchMapping("/{id}")
public ResponseEntity<T> partialUpdate(@PathVariable ID id, @RequestBody T updates) {
return service.getById(id)
.map(entity -> {
// Apply partial updates to entity here
service.update(id, entity);
return new ResponseEntity<>(entity, HttpStatus.OK);
})
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable ID id) {
if (!service.existsById(id)) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
service.delete(id);
return new ResponseEntity<>(HttpStatus.OK);
}
}

Step 6: Create a Resource Controller

Finally, we create a resource controller that will use our generic CRUD service to handle CRUD requests. Here is an example:

import com.sample.springcrudgeneric.crud.CrudController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/employees")
public class EmployeeController extends CrudController<Employee, String> {

public EmployeeController(EmployeeService service) {
super(service);
}
}

Conclusion:

In this blog post, we have learned how to create a generic CRUD controller in Spring Boot. By following this approach, we can reduce code duplication and improve code maintainability in our Spring Boot applications. This generic CRUD controller can be easily extended to support CRUD operations for other resource types as well.

--

--

Mayank Yaduvanshi
Mayank Yaduvanshi

Written by Mayank Yaduvanshi

Full Stack Software Developer, Experienced in Angular, Flutter, Spring boot, NodeJs, MySQL, MongoDB, AWS, Google Cloud, Docker, Git, JIRA.

No responses yet