fbpx

Spring Boot MongoDB REST API CRUD with Kotlin

1. Introduction

Welcome to the 19th post on my blog. This time, I would like to show you how to create a simple Spring Boot MongoDB REST API CRUD with Kotlin.

After finishing this article, you will have a fundamental understanding of the process of building Spring Boot applications utilizing Spring Data MongoDB to connect with MongoDB.

2. Run MongoDB Server

Before we start coding, we need to make sure, that our MongoDB server is up and running. In this article, I would like to focus on the Spring Boot part, so we will deploy it as a docker container. However, if you would like to install a MongoDB on your local machine, please refer to this official manual.

2.1. Deploy MongoDB as a Container

As the first step, let’s pull the mongo image from the Docker Hub registry:

docker pull mongo

After the image is pulled, let’s deploy it in a detached mode ( -d flag):

docker run -d -p 27017-27019:27017-27019 --name mongodb mongo

Besides deploying in a detached mode, the above command publishes the container’s ports to the host- in simple terms, we will be able to connect to the MongoDB using localhost.

To validate, if everything is OK, let’s run the following command:

docker ps

#command output
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                  NAMES
26bd5c8dcc79        mongo               "docker-entrypoint.s…"   6 seconds ago       Up 5 seconds        0.0.0.0:27017-27019->27017-27019/tcp   mongodb

3. Imports

As the next step, let’s create a Spring Boot project (for instance, using start.spring.io) and let’s add the following imports:

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")

With these two imports, we will be able to create a REST API and integrate it with the MongoDB document database.

4. Configure Application Properties

One of the things, I’d like to show you in this tutorial will be the implementation of a custom exception class with a message. Since the Spring Boot 2.3 release, error messages are no longer included in the server’s responses by default. To change that, let’s add the following lines to the application.yaml file:

server:
  error:
    include-message: always

5. Create Models

After all of the above being done, we can start defining models, which will be used to persist and fetch objects from the database. In our example, we will define two separate classes- a Company and an Employee:

@Document("companies")
data class Company(
    @Id
    val id: String? = null,
    var name: String,
    @Field("company_address")
    var address: String
)
data class Employee(
    @Id
    val id: ObjectId? = null,
    var firstName: String,
    var lastName: String,
    var email: String,
    var company: Company?
)

As you might have noticed, the Company class definition has two (optional) annotations more, than the Employee. Let me explain them:

  • a @Document annotation- usually, we will use it to change the default name of the MongoDB collection used to store the document. By default, the name of the collection will be the class name changed to start with a lower-case letter. So, in our database, we will have two collections created: companies and employee.
  • a @Field annotation- it’s used to define custom metadata for the document fields, like a key name, order, or a target type. In our example, we’ve changed the key name from the default “address” to the “company_address”.

One more thing, I would like to add is that by default MongoDB uses ObjectIds as the default value of the _id field of each document. An ObjectId is a 12-byte BSON type having the following structure:

  • The first 4 bytes represent the seconds since the Unix epoch
  • The next 3 bytes are the machine identifier
  • The next 2 bytes consists of process id
  • The last 3 bytes are a random counter value

These 12 bytes altogether uniquely identify a document within the MongoDB collection and serve as a primary key for that document as well. However, from the Spring Boot perspective, we can treat them as ObjectIds or Strings– the decision is up to you.

6. Create Custom Exception

Nextly, let’s create a custom exception class named NotFoundException:

@ResponseStatus(HttpStatus.NOT_FOUND)
class NotFoundException(msg: String) : RuntimeException(msg)

The @ResponseStatus annotation allows us to specify the status code to use for the response- in this case- 404 Not Found.

7. Create Repositories

As the next step, let’s create repositories for our data. Let’s start by adding the CompanyRepository first:

interface CompanyRepository : MongoRepository<Company, String>

As you can see, the CompanyRepository interface extends the MongoRepository, which provides us with basic CRUD operations. Similarly, let’s create the EmployeeRepository:

interface EmployeeRepository : MongoRepository<Employee, ObjectId> {
    fun findByCompanyId(id: String): List<Employee>
}

As you might have noticed, this time we’ve provided additionally the definition of findByCompanyId function, which will be used later to find employees of a specific company.

8. Create Services

Nextly, let’s implement the service layer for our application.

8.1. Implement CompanyService

Before we will create the service, let’s add the CompanyRequest class, which will be used to store the data passed by user:

class CompanyRequest(
    val name: String,
    val address: String
)

Nextly, let’s create the CompanyService class, which will allow us to inject previously created repositories:

@Service
class CompanyService(
    private val companyRepository: CompanyRepository,
    private val employeeRepository: EmployeeRepository
) {}

With that being done, we can add functions responsible for company data management:

fun createCompany(request: CompanyRequest): Company =
    companyRepository.save(
        Company(
            name = request.name,
            address = request.address
        )
    )

fun findAll(): List<Company> =
    companyRepository.findAll()

fun findById(id: String): Company =
    companyRepository.findById(id)
        .orElseThrow { NotFoundException("Company with id $id not found") }

fun updateCompany(id: String, request: CompanyRequest): Company {
    val companyToUpdate = findById(id)

    val updatedCompany = companyRepository.save(
        companyToUpdate.apply {
            name = request.name
            address = request.address
        }
    )

    updateCompanyEmployees(updatedCompany)

    return updatedCompany
}

fun deleteById(id: String) {
    val companyToDelete = findById(id)

    companyRepository.delete(companyToDelete)
}

private fun updateCompanyEmployees(updatedCompany: Company) {
    employeeRepository.saveAll(
        employeeRepository.findByCompanyId(updatedCompany.id!!)
            .map { it.apply { company = updatedCompany } }
    )
}

Basically, these are just simple functions responsible for creating, deleting, and retrieving the data through the CompanyRepository.

However, you might have noticed that each time, we would like to update the company, we need to update all employees connected with it. That’s because we keep the related company in denormalized form- inside the employee document. To visualize that, let’s check the example document from our database:

{
    "firstName": "Piotr",
    "lastName": "Wolak",
    "email": "contact@codersee.com",
    "company": {
        "_id": {
            "$oid": "5fcb90f830e3af4497f5de14"
        },
        "name": "Company Two",
        "company_address": "Address Two"
    },
    "_class": "com.codersee.mongocrud.model.Employee"
}

As you can see, the company document is stored as an inner document of each employee. This might seem pretty weird, especially when you have any previous experience working with SQL databases, but this is the optimal choice for most real-life scenarios when using MongoDB (if you would like to learn more about MongoDB relationships, please let me know and I will create another tutorial about it :).

8.2. Implement EmployeeService

Similarly, let’s create the EmployeeRequest responsible for data handling and the EmployeeService:

class EmployeeRequest(
    val firstName: String,
    val lastName: String,
    val email: String,
    val companyId: String?
)
@Service
class EmployeeService(
    private val companyService: CompanyService,
    private val employeeRepository: EmployeeRepository
) {

    fun createEmployee(request: EmployeeRequest): Employee {
        val company = request.companyId?.let { companyService.findById(it) }

        return employeeRepository.save(
            Employee(
                firstName = request.firstName,
                lastName = request.lastName,
                email = request.email,
                company = company
            )
        )
    }
>
    fun findAll(): List<Employee> =
        employeeRepository.findAll()

    fun findAllByCompanyId(id: String): List<Employee> =
        employeeRepository.findByCompanyId(id)

    fun findById(id: ObjectId): Employee =
        employeeRepository.findById(id)
            .orElseThrow { NotFoundException("Employee with id $id not found") }

    fun updateEmployee(id: ObjectId, request: EmployeeRequest): Employee {
        val employeeToUpdate = findById(id)
        val foundCompany = request.companyId?.let { companyService.findById(it) }

        return employeeRepository.save(
            employeeToUpdate.apply {
                firstName = request.firstName
                lastName = request.lastName
                email = request.email
                company = foundCompany
            }
        )
    }

    fun deleteById(id: ObjectId) {
        val employeeToDelete = findById(id)

        employeeRepository.delete(employeeToDelete)
    }
}

9. Implement REST Controllers

As the last step, we will implement the controller layer responsible for request handling. But before that, let’s prepare two classes: CompanyResponse and EmployeeResponse responsible for returning the data to the user.

Important note: although the creation of separate classes for transferring the incoming and outgoing data adds some redundancy to the code, I personally believe it is a good way to separate these concepts and make the code cleaner.

class CompanyResponse(
    val id: String,
    val name: String,
    val address: String
) {
    companion object {
        fun fromEntity(company: Company): CompanyResponse =
            CompanyResponse(
                id = company.id!!,
                name = company.name,
                address = company.address
            )
    }
}
class EmployeeResponse(
    val id: String,
    val firstName: String,
    val lastName: String,
    val email: String,
    val company: CompanyResponse?
) {
    companion object {
        fun fromEntity(employee: Employee): EmployeeResponse =
            EmployeeResponse(
                id = employee.id!!.toHexString(),
                firstName = employee.firstName,
                lastName = employee.lastName,
                email = employee.email,
                company = employee.company?.let { CompanyResponse.fromEntity(it) }
            )
    }
}

AS you can see, both classes contain fromEntity functions inside companion objects. As the name suggests, they will be used to convert entity objects to response instances.

9.1. Create CompanyController

Let’s add the CompanyController class to our project first:

@RestController
@RequestMapping("/api/company")
class CompanyController(
    private val companyService: CompanyService
) {

    @PostMapping
    fun createCompany(@RequestBody request: CompanyRequest): ResponseEntity<CompanyResponse> {
        val createdCompany = companyService.createCompany(request)

        return ResponseEntity
            .ok(
                CompanyResponse.fromEntity(createdCompany)
            )
    }

    @GetMapping
    fun findAllCompanies(): ResponseEntity<List<CompanyResponse>> {
        val companies = companyService.findAll()

        return ResponseEntity
            .ok(
                companies.map { CompanyResponse.fromEntity(it) }
            )
    }

    @GetMapping("/{id}")
    fun findCompanyById(@PathVariable id: String): ResponseEntity<CompanyResponse> {
        val company = companyService.findById(id)

        return ResponseEntity
            .ok(
                CompanyResponse.fromEntity(company)
            )
    }

    @PutMapping("/{id}")
    fun updateCompany(
        @PathVariable id: String,
        @RequestBody request: CompanyRequest
    ): ResponseEntity<CompanyResponse> {
        val updatedCompany = companyService.updateCompany(id, request)

        return ResponseEntity
            .ok(
                CompanyResponse.fromEntity(updatedCompany)
            )
    }

    @DeleteMapping("/{id}")
    fun deleteCompany(@PathVariable id: String): ResponseEntity<Void> {
        companyService.deleteById(id)

        return ResponseEntity.noContent().build()
    }
}

9.2. Create EmployeeController

Similarly, let’s implement the EmployeeController:

@RestController
@RequestMapping("/api/employee")
class EmployeeController(
    private val employeeService: EmployeeService
) {

    @PostMapping
    fun createEmployee(@RequestBody request: EmployeeRequest): ResponseEntity<EmployeeResponse> {
        val createdEmployee = employeeService.createEmployee(request)

        return ResponseEntity
            .ok(
                EmployeeResponse.fromEntity(createdEmployee)
            )
    }

    @GetMapping
    fun findAllEmployees(): ResponseEntity<List<EmployeeResponse>> {
        val employees = employeeService.findAll()

        return ResponseEntity
            .ok(
                employees.map { EmployeeResponse.fromEntity(it) }
            )
    }

    @GetMapping("/{id}")
    fun findEmployeeById(@PathVariable id: ObjectId): ResponseEntity<EmployeeResponse> {
        val employee = employeeService.findById(id)

        return ResponseEntity
            .ok(
                EmployeeResponse.fromEntity(employee)
            )
    }

    @GetMapping("/company/{companyId}")
    fun findAllByCompanyId(@PathVariable companyId: String): ResponseEntity<List<EmployeeResponse>> {
        val employees = employeeService.findAllByCompanyId(companyId)

        return ResponseEntity
            .ok(
                employees.map { EmployeeResponse.fromEntity(it) }
            )
    }

    @PutMapping("/{id}")
    fun updateUpdateEmployee(
        @PathVariable id: ObjectId,
        @RequestBody request: EmployeeRequest
    ): ResponseEntity<EmployeeResponse> {
        val updatedEmployee = employeeService.updateEmployee(id, request)

        return ResponseEntity
            .ok(
                EmployeeResponse.fromEntity(updatedEmployee)
            )
    }

    @DeleteMapping("/{id}")
    fun deleteEmployee(@PathVariable id: ObjectId): ResponseEntity<Void> {
        employeeService.deleteById(id)

        return ResponseEntity.noContent().build()
    }
}

10. Test with CURL

Finally, we can run our application and test the endpoints using cURL. If you would like to explore the database, I highly recommend the MongoDB Compass, which is the dedicated GUI for MongoDB.

Let’s start by adding two companies:

# 1st company
curl --location --request POST 'localhost:8080/api/company' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Company 1",
    "address": "Address 1"
}'

# 2nd company
curl --location --request POST 'localhost:8080/api/company' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Company 2",
    "address": "Address 2"
}'

To validate, let’s list all companies:

curl --location --request GET 'localhost:8080/api/company'

# Example output
[
  {
    "id": "5fcba07a30e3af4497f5de16",
    "name": "Company 1",
    "address": "Address 1"
  },
  {
    "id": "5fcba07c30e3af4497f5de17",
    "name": "Company 2",
    "address": "Address 2"
  }
]

If we would like to find a specific company, then let’s use the following command:

curl --location --request GET 'localhost:8080/api/company/5fcba07c30e3af4497f5de17'

# Expected output
{
  "id": "5fcba07c30e3af4497f5de17",
  "name": "Company 2",
  "address": "Address 2"
}

To update the company, let’s use the PUT request:

curl --location --request PUT 'localhost:8080/api/company/5fcba07c30e3af4497f5de17' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Company 2 Updated",
    "address": "Address 2 Updated"
}'

If everything went well, we should see that our company data has been updated- you can always check it with another endpoint as well.

To delete the company, let’s use the DELETE handling endpoint:

curl --location --request DELETE 'localhost:8080/api/company/5fcba07c30e3af4497f5de17'

This time, we should receive an empty response body with 204 No Content status and a company should be deleted as well.

Similarly, we can test the rest of the endpoints responsible for employees management- let it be homework for you- I’ve always believed there is no other way to learn anything, than trying on our own 🙂

11. Summary

And that would be all for this article. We’ve gone step by step through the process of deploying the MongoDB and a Docker container and creating the Spring Boot MongoDB REST API CRUD with Kotlin.

I hope, you really enjoyed it and that my blog helps you to understand programming better. If you would like to ask about anything or add your own suggestions, please do it in the comment section below, or by using the contact form.

Traditionally, you can find the working project in this GitHub repository.

Share on facebook
Share on twitter
Share on linkedin

2 thoughts on “Spring Boot MongoDB REST API CRUD with Kotlin”

  1. Hello, thank you for the tutorial. You forgot to mention the setting up of the db connection in application.properties.
    Here’s the code for db connection:
    spring.data.mongodb.database=mongodb
    spring.data.mongodb.port = 27017

    1. Hi,
      You are totally right, we can configure these settings inside the application properties file. But the great thing about the Spring Boot, that if we would like to use these default settings (mongodb/27017), we do not specify them at all-it will do that for us underneath 🙂
      All the magic happens, because of this line in gradle:
      implementation(“org.springframework.boot:spring-boot-starter-data-mongodb”)

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Join the community and get free eBooks.

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy

Find us also on...

Join the FREE weekly newsletter and get two free eBooks as well:

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.