The Back-End
Cloud
The architecture
CRUDs

CRUD development cycle

Most web applications provide a way to create, read, update, and delete data. This is often referred to as CRUD. In this section, we'll look at how to implement CRUD functionality in a Spring Boot application.

Components

  • Model - an entity class that models the data of the application. In our case, it's a Book class.
  • Repository - an interface that provides methods to perform CRUD operations. In our case, it's a BookRepository interface.
  • DTO - a class that models the data for the client. In our case, it's a BookDTO class.
  • Service - a class that provides methods to perform CRUD operations. In our case, it's a BookService class.
  • Controller - a class that handles HTTP requests. In our case, it's a BookController class.

Prerequisites

  • We use MongoDB and Spring Data MongoDB to store and retrieve data. So, we need to add the spring-boot-starter-data-mongodb dependency to the build.gradle.kts file.
  • Use DRY principle. Don't Repeat Yourself.
  • Before you start, switch to dev branch, pull, and create a branch starting with the JIRA ticket number. For example, ASRR-1/create-book-component.

File Structure

src
├── main
│   ├── kotlin
│   │   └── nl
│   │       └── asrr
│   │           └── demo
│   │               ├── book
│   │               │   ├── controller
│   │               │   │   └── BookController.kt
│   │               │   ├── model
│   │               │   │   └── Book.kt
│   │               │   ├── repository
│   │               │   │   └── BookRepository.kt
│   │               │   ├── service
│   │               │   │   └── BookService.kt
│   │               ├── DemoApplication.kt

You should create a package for each component. In our case, we have a book package for the book component. The book package contains a controller, model, repository, and service package.

Model

We use a Kotlin data class to define our models. This has the advantage of automatically generating getters, setters, equals(), hashCode(), toString(), and copy() methods.

The Book class is an entity class that models the data of the application. It has the following fields:

  • id - a unique identifier of the book.
  • title - a title of the book.
  • author - an author of the book.
  • year - a year of the book.
@Document(collection = "books")
data class Book(
    @Id
    val id: String,
    val title: String,
    val author: String,
    val year: Int,
    val createdAt: LocalDateTime = LocalDateTime.now()
    val createdBy: String = "system",
    val updatedAt: LocalDateTime = LocalDateTime.now()
    val updatedBy: String = "system"
)

Notice the @Id annotation on the id field. This annotation is used to mark the field as a primary key. The @Document annotation is used to mark the class as a document that will be stored in the database.

Repository

We use a Kotlin interface to define our repositories. This has the advantage of automatically generating the implementation of the interface.

The BookRepository interface provides methods to perform CRUD operations. It extends the MongoRepository interface that provides methods to perform CRUD operations on a MongoDB database.

@Repository
interface BookRepository : MongoRepository<Book, String>

The @Repository annotation is used to mark the class as a repository. The String in your MongoRepository is the type of the id field of your entity class.

You can expand these methods by adding a custom method. For example, you can add a method to find a book by title.

@Repository
interface BookRepository : MongoRepository<Book, String> {
    fun findByTitle(title: String): Book
    fun existsByTitle(title: String): Boolean
}

Depending on the props in your model class, Spring Data MongoDB will automatically generate a query for you. In this case, it will generate a query to find a book by title. You can also write your own query.

@Repository
interface BookRepository : MongoRepository<Book, String> {
    @Query("{ 'title' : ?0 }")
    fun findByTitle(title: String): Book
}

DTO

A DTO (Data Transfer Object) is a class that models the data for the client. It's a good practice to use DTOs to transfer data between layers. In our case, it's a BookDTO class.

data class BookDTO(
    val title: String,
    val author: String,
    val year: Int
)

Note that it is missing the id field. This is because we don't want to let the client determine the id. The id field is returned in the full object, which is immutable to the front-end. Note that it is missing the createdAt, createdBy, updatedAt, and updatedBy fields. This is because we don't want to let the client determine these fields. These fields are set by the back-end.

Service

The service class contains all business logic. It provides methods to perform CRUD operations. In our case, it's a BookService class.

@Service
fun BookService(
    private val bookRepository: BookRepository, private val idGenerator: IdGenerator // asrr id generator
) {
    fun findAll(): List<Book> = bookRepository.findAll()
 
    fun findById(id: String): Book = bookRepository.findById(id).orElseThrow { BookNotFoundException(id) }
 
    fun create(bookDTO: BookDTO): Book = {
        // check for duplicate title
        if (bookRepository.existsByTitle(bookDTO.title)) {
            throw BookAlreadyExistsException(bookDTO.title)
        }
 
        // create book
        val book = Book(
            id = idGenerator.generateId(), // use asrr id generator
            title = bookDTO.title,
            author = bookDTO.author,
            year = bookDTO.year
        )
 
        return bookRepository.save(book) // return so that front-end can receive values after save
    }
 
    fun update(id: String, bookDTO: BookDTO): Book {
        val book = findById(id)
 
        // check for duplicate title
        if (book.title != bookDTO.title && bookRepository.existsByTitle(bookDTO.title)) {
            throw BookAlreadyExistsException(bookDTO.title)
        }
 
        book.title = bookDTO.title
        book.author = bookDTO.author
        book.year = bookDTO.year
        return bookRepository.save(book)
    }
 
    fun delete(id: String) = bookRepository.deleteById(id)
}

Controller

The controller class handles HTTP requests. It calls the service class to perform CRUD operations. In our case, it's a BookController class.

@RestController
@Tag(name = "books", description = "the book API") // swagger information
@RequestMapping("/books") // api path
fun BookController(
    private val bookService: BookService
) {
    @GetMapping
    @Operation(summary = "Get all books", description = "Get all books")
    fun findAll(): List<Book> = bookService.findAll()
 
    @GetMapping("/{id}")
    @Operation(summary = "Get a book by id", description = "Get a book by id")
    fun findById(@PathVariable id: String): Book = bookService.findById(id)
 
    @PostMapping
    @Operation(summary = "Create a book", description = "Create a book")
    fun create(@RequestBody bookDTO: BookDTO): Book = bookService.create(bookDTO)
 
    @PutMapping("/{id}")
    @Operation(summary = "Update a book", description = "Update a book")
    fun update(@PathVariable id: String, @RequestBody bookDTO: BookDTO): Book = bookService.update(id, bookDTO)
 
    @DeleteMapping("/{id}")
    @Operation(summary = "Delete a book", description = "Delete a book")
    fun delete(@PathVariable id: String) = bookService.delete(id)
}

You should wrap your responsees with ResponseEntity to return the correct HTTP status code. For example, you should return HttpStatus.NOT_FOUND when the book is not found.

@RestController
@RequestMapping("/books")
fun BookController(
    private val bookService: BookService
) {
    @GetMapping("/{id}")
    fun findById(@PathVariable id: String): ResponseEntity<Book> {
        val book = bookService.findById(id)
        return ResponseEntity.ok(book)
    }
}

We can define custom exception handlers in Spring Boot. We will look at this in a later course.