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 and if you enjoyed this article, then you might want to learn about reactive REST API with MongoDB, as well here.
2 Responses
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
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”)