Spring Boot REST API CRUD with Kotlin and MongoDB

1. Introduction

2. Run MongoDB Server

2.1. Deploy MongoDB as a Container

docker pull mongo
docker run -d -p 27017-27019:27017-27019 --name mongodb mongo
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

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

4. Configure Application Properties

server:
error:
include-message: always

5. Create Models

@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?
)

6. Create Custom Exception

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

7. Create Repositories

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

8. Create Services

8.1. Implement CompanyService

class CompanyRequest(
val name: String,
val address: String
)
@Service
class CompanyService(
private val companyRepository: CompanyRepository,
private val employeeRepository: EmployeeRepository
) {}
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 } }
)
}
{
"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"
}

8.2. Implement 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

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) }
)
}
}

9.1. Create CompanyController

@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

@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

# 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"
}'
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"
}
]
curl --location --request GET 'localhost:8080/api/company/5fcba07c30e3af4497f5de17'

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

11. Summary

Hi, my name is Piotr and I am the founder of Codersee- a technical blog, where I am teaching programming by practical step by step tutorials.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store