Codersee
Kotlin on the backend
Codersee
Kotlin on the backend
In this practical guide, I will show you how to create a reactive REST API using Spring, Kotlin, coroutines, and Kotlin Flows.
Hello friend! In this, practical step-by-step guide, I will teach you how to create a reactive REST API using Spring, Kotlin, coroutines, and Kotlin Flows entirely from scratch.
And although Spring internally uses Reactor implementation, coroutines provide a more straightforward and natural way of writing asynchronous, non-blocking code. Thanks to this, we can enjoy the benefits of a non-blocking code without compromising the readability of the code (which might become an issue when using Project Reactor in more mature and complex projects).
At the end of this tutorial, you will know precisely how to:
So, without any further ado, let’s get to work 🙂
If you prefer video content, then check out my video:
If you find this content useful, please leave a subscription 😉
As the first step, let’s learn how to set up a fresh PostgreSQL instance using Docker and populate it with the necessary data. Of course, feel free to skip step 2.1 if you already have some development database installed.
In order to start a fresh instance, let’s navigate to the terminal and specify the following command:
docker run --name some-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres
After that, let’s wait a while until the database is up and running.
We can additionally verify if that’s the case using docker ps
:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1863e6ec964a postgres "docker-entrypoint.s…" 6 hours ago Up About an hour 0.0.0.0:5432->5432/tcp some-postgres
This one, simple command results in a couple of interesting things happening underneath:
-d
flag.[elementor-template id=”9007393″]
As the next step, let’s connect to the database, and create a schema with two tables:
create schema if not exists application; create table if not exists application.company( id serial not null primary key, name varchar(255) not null, address varchar(255) not null ); create table if not exists application.app_user( id serial not null primary key, email varchar(255) not null unique, name varchar(255) not null, age int not null, company_id bigint not null references application.company(id) on delete cascade );
As we can see, the purpose of our application will be users and companies management. Each user will have to be assigned to some company. Moreover, if a company is deleted, then the related user rows will be removed, as well.
Additionally, we can populate tables with the following script:
insert into application.company(name, address) values ('Company 1', 'Address 1'), ('Company 2', 'Address 2'), ('Company 3', 'Address 3'); insert into application.app_user(email, name, age, company_id) values ('email-1@codersee.com', 'John', 23, 1), ('email-2@codersee.com', 'Adam', 33, 1), ('email-3@codersee.com', 'Maria', 40, 2), ('email-4@codersee.com', 'James', 39, 3), ('email-5@codersee.com', 'Robert', 41, 3), ('email-6@codersee.com', 'Piotr', 28, 3);
With that being done, let’s navigate to the Spring Initializr page and generate a new project:
The above configuration is all we need in order to create a fresh Spring Boot 3 project with Kotlin and coroutines. Additionally, in order to connect to the Postgres database, we need two more dependencies: Spring Data R2DBC and PostgreSQL Driver.
With that being done, let’s hit the Generate button and import the project to our IDE (you can find a video on how to configure IntelliJ IDEA for Kotlin right here).
Nextly, let’s open up the application.properties
file, change its extension to .yaml
, and insert the connection config:
spring: r2dbc: url: r2dbc:postgresql://${DB_HOST:localhost}:5432/ username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:postgres}
This configuration instructs Spring to check DB_HOST, DB_USERNAME, and DB_PASSWORD environment variables first. If a particular variable is missing, then we provide the default values– localhost and postgres.
Following, let’s create a new package called model
and introduce classes responsible for mapping database tables.
As the first one, let’s implement the Company:
@Table("application.company") data class Company( @Id val id: Long? = null, val name: String, val address: String )
The @Table and @Id annotations are pretty descriptive and they are necessary in order to configure mapping in Spring. Nevertheless, it’s worth mentioning that if we do not want to generate identifiers manually, then the identifier fields have to be nullable.
Similarly, let’s create the User data class:
@Table("application.app_user") data class User( @Id val id: Long? = null, val email: String, val name: String, val age: Int, val companyId: Long )
Moving forward, let’s create the repository
package.
In our project, we will utilize the CoroutineCrudRepository
– a dedicated Spring Data repository built on Kotlin coroutines. If you’ve ever been working with Reactor, then in a nutshell, Mono<T> functions are replaced with suspended functions returning the type T, and instead of creating Fluxes, we will generate Flows. On the other hand, if you have never worked with Reactor, then Flow<T> return type means that a function returns multiple asynchronously computed values suspend function returns only a single value.
Of course, this is a simplification and if you would like to learn the differences between Fluxes and Flows, then let me know in the comments.
To kick things off, let’s implement the UserRepository interface:
interface UserRepository : CoroutineCrudRepository<User, Long> { fun findByNameContaining(name: String): Flow<User> fun findByCompanyId(companyId: Long): Flow<User> @Query("SELECT * FROM application.app_user WHERE email = :email") fun randomNameFindByEmail(email: String): Flow<User> }
The CoroutineCrudRepository
extends the Spring Data Repository
and requires us to provide two types: the domain type and the identifier type- a User and a Long in our case. This interface comes with 15 already implemented functions, like save, findAll, delete, etc.- responsible for generic CRUD operations. This way, we can tremendously reduce the amount of boilerplate in our Kotlin codebase.
Moreover, we make use of two, great features of Spring Data (which are not Kotlin, or coroutines specific):
findByNameContaining
will be translated into where like..
query and findByCompanyId
will let us search users by company identifier.Note: I’ve named the third method randomNameFindByEmail just to emphasize, that the function name is irrelevant when using the Query, don’t do that in your codebase 😀
Nextly, let’s add the CompanyRepository with only one custom function:
@Repository interface CompanyRepository : CoroutineCrudRepository<Company, Long> { fun findByNameContaining(name: String): Flow<Company> }
With model and repository layer implemented, we can move on and create a service
package.
Firstly, let’s add the UserService
to our project:
@Service class UserService( private val userRepository: UserRepository ) { suspend fun saveUser(user: User): User? = userRepository.randomNameFindByEmail(user.email) .firstOrNull() ?.let { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "The specified email is already in use.") } ?: userRepository.save(user) suspend fun findAllUsers(): Flow<User> = userRepository.findAll() suspend fun findUserById(id: Long): User? = userRepository.findById(id) suspend fun deleteUserById(id: Long) { val foundUser = userRepository.findById(id) if (foundUser == null) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User with id $id was not found.") else userRepository.deleteById(id) } suspend fun updateUser(id: Long, requestedUser: User): User { val foundUser = userRepository.findById(id) return if (foundUser == null) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User with id $id was not found.") else userRepository.save( requestedUser.copy(id = foundUser.id) ) } suspend fun findAllUsersByNameLike(name: String): Flow<User> = userRepository.findByNameContaining(name) suspend fun findUsersByCompanyId(id: Long): Flow<User> = userRepository.findByCompanyId(id) }
All the magic starts with the @Service annotation, which is a specialization of a @Component. This way, we simply instruct Spring to create a bean of UserService.
As we can clearly see, our service logic is really straightforward, and thanks to the coroutines we can write code similar to imperative programming.
Lastly, I just wanted to mention the logic responsible for User updates. The save
method of the Repository
interface works in two ways:
Following, let’s implement the CompanyService
responsible for companies management:
@Component class CompanyService( private val companyRepository: CompanyRepository ) { suspend fun saveCompany(company: Company): Company? = companyRepository.save(company) suspend fun findAllCompanies(): Flow<Company> = companyRepository.findAll() suspend fun findCompanyById(id: Long): Company? = companyRepository.findById(id) suspend fun deleteCompanyById(id: Long) { val foundCompany = companyRepository.findById(id) if (foundCompany == null) throw ResponseStatusException(HttpStatus.NOT_FOUND, "Company with id $id was not found.") else companyRepository.deleteById(id) } suspend fun findAllCompaniesByNameLike(name: String): Flow<Company> = companyRepository.findByNameContaining(name) suspend fun updateCompany(id: Long, requestedCompany: Company): Company { val foundCompany = companyRepository.findById(id) return if (foundCompany == null) throw ResponseStatusException(HttpStatus.NOT_FOUND, "Company with id $id was not found.") else companyRepository.save( requestedCompany.copy(id = foundCompany.id) ) } }
And the last thing we need to implement in our Spring Kotlin Coroutines project are… REST endpoints (and a couple of DTOs 😉 ).
When working in real-life scenarios we can use different approaches, when it comes to serialization and deserialization of data (or in simple terms- JSON <-> Kotlin objects conversions). In some cases dealing with model classes might be sufficient, but introducing DTOs will usually be a better approach. In our examples, we will introduce separate request and response classes, which in my opinion let us maintain our codebase much easier.
To do so, let’s add two data classes to our codebase- the UserRequest
and UserResponse
(inside the dto
package):
data class UserRequest( val email: String, val name: String, val age: Int, @JsonProperty("company_id") val companyId: Long ) data class UserResponse( val id: Long, val email: String, val name: String, val age: Int )
Request classes will be used to translate JSON payload into Kotlin objects, whereas the response ones will do the opposite.
Additionally, we make use of the @JsonProperty
annotation, so that our JSON files will use the snake case.
With that prepared, we have nothing else to do than create a controller
package and implement the UserController:
@RestController @RequestMapping("/api/users") class UserController( private val userService: UserService ) { @PostMapping suspend fun createUser(@RequestBody userRequest: UserRequest): UserResponse = userService.saveUser( user = userRequest.toModel() ) ?.toResponse() ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during user creation.") @GetMapping suspend fun findUsers( @RequestParam("name", required = false) name: String? ): Flow<UserResponse> { val users = name?.let { userService.findAllUsersByNameLike(name) } ?: userService.findAllUsers() return users.map(User::toResponse) } @GetMapping("/{id}") suspend fun findUserById( @PathVariable id: Long ): UserResponse = userService.findUserById(id) ?.let(User::toResponse) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User with id $id was not found.") @DeleteMapping("/{id}") suspend fun deleteUserById( @PathVariable id: Long ) { userService.deleteUserById(id) } @PutMapping("/{id}") suspend fun updateUser( @PathVariable id: Long, @RequestBody userRequest: UserRequest ): UserResponse = userService.updateUser( id = id, requestedUser = userRequest.toModel() ) .toResponse() } private fun UserRequest.toModel(): User = User( email = this.email, name = this.name, age = this.age, companyId = this.companyId ) fun User.toResponse(): UserResponse = UserResponse( id = this.id!!, email = this.email, name = this.name, age = this.age )
I know, the class itself might be a little big, but let’s break it down into parts first. We have a couple of annotations here, so why don’t we start with them?
The @RestController is nothing else, then a combination of a @Controller– informing Spring that our class is a web controller and a @ResponseBody- which indicates that the things our functions return should be bound to the web response body (simply- returned to the API user).
The @RequestMapping allows us to specify the path, to which our class will respond. So, each time we will hit the localhost:8080/api/users
, Spring will search for a handler function inside this class.
On the other hand, the @PostMapping, @GetMapping, etc. simply indicate for which HTTP methods a particular function should be invoked (and also can take the additional path segments).
Lastly, the @RequestParam, @PathVariable, and @RequestBody are used to map request parameters, segment paths, and body payload to Kotlin class instances.
The rest of the code is responsible for invoking our service methods and throwing meaningful errors when something is wrong (with a help of extension functions used to map between models and responses).
Similarly, let’s add response and request classes for Company resources:
data class CompanyRequest( val name: String, val address: String ) data class CompanyResponse( val id: Long, val name: String, val address: String, val users: List<UserResponse> )
And this time, let’s add the CompanyController
class:
@RestController @RequestMapping("/api/companies") class CompanyController( private val companyService: CompanyService, private val userService: UserService ) { @PostMapping suspend fun createCompany(@RequestBody companyRequest: CompanyRequest): CompanyResponse = companyService.saveCompany( company = companyRequest.toModel() ) ?.toResponse() ?: throw ResponseStatusException( HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during company creation." ) @GetMapping suspend fun findCompany( @RequestParam("name", required = false) name: String? ): Flow<CompanyResponse> { val companies = name?.let { companyService.findAllCompaniesByNameLike(name) } ?: companyService.findAllCompanies() return companies .map { company -> company.toResponse( users = findCompanyUsers(company) ) } } @GetMapping("/{id}") suspend fun findCompanyById( @PathVariable id: Long ): CompanyResponse = companyService.findCompanyById(id) ?.let { company -> company.toResponse( users = findCompanyUsers(company) ) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Company with id $id was not found.") @DeleteMapping("/{id}") suspend fun deleteCompanyById( @PathVariable id: Long ) { companyService.deleteCompanyById(id) } @PutMapping("/{id}") suspend fun updateCompany( @PathVariable id: Long, @RequestBody companyRequest: CompanyRequest ): CompanyResponse = companyService.updateCompany( id = id, requestedCompany = companyRequest.toModel() ) .let { company -> company.toResponse( users = findCompanyUsers(company) ) } private suspend fun findCompanyUsers(company: Company) = userService.findUsersByCompanyId(company.id!!) .toList() } private fun CompanyRequest.toModel(): Company = Company( name = this.name, address = this.address ) private fun Company.toResponse(users: List<User> = emptyList()): CompanyResponse = CompanyResponse( id = this.id!!, name = this.name, address = this.address, users = users.map(User::toResponse) )
And although this controller class looks similar, I wanted to emphasize two things:
As the last thing, I wanted to show you how easily we can merge two Flows. And to do so, let’s introduce a new search endpoint, which will be used to return both users and companies by their names.
So firstly, let’s add the IdNameTypeResponse
:
data class IdNameTypeResponse( val id: Long, val name: String, val type: ResultType ) enum class ResultType { USER, COMPANY }
Moving forward, let’s implement the SearchController
:
@RestController @RequestMapping("/api/search") class SearchController( private val userService: UserService, private val companyService: CompanyService ) { @GetMapping suspend fun searchByNames( @RequestParam(name = "query") query: String ): Flow<IdNameTypeResponse> { val usersFlow = userService.findAllUsersByNameLike(name = query) .map(User::toIdNameTypeResponse) val companiesFlow = companyService.findAllCompaniesByNameLike(name = query) .map(Company::toIdNameTypeResponse) return merge(usersFlow, companiesFlow) } } private fun User.toIdNameTypeResponse(): IdNameTypeResponse = IdNameTypeResponse( id = this.id!!, name = this.name, type = ResultType.USER ) private fun Company.toIdNameTypeResponse(): IdNameTypeResponse = IdNameTypeResponse( id = this.id!!, name = this.name, type = ResultType.COMPANY )
As we can see, in order to combine together both user and company results we can use the merge function. This way, our flows are merged concurrently (and without preserving the order), without limit on the number of simultaneously collected flows.
At this point, we have everything we need to start testing, so let it be your homework. Going through all of the handler methods and preparing appropriate requests will be a great opportunity to recap everything we learned today 🙂
As a bonus- right here you can find a ready-to-go Postman collection, which you can import to your computer.
And that’s all for this hands-on tutorial, in which we’ve learned together how to create a REST API using Spring, Kotlin, coroutines, and Kotlin Flows. As always, you can find the whole project in this GitHub repository.
If you’re interested in learning more about the reactive approach, then check out my other materials in the Spring Webflux tag.
I hope you enjoyed this article and will be forever thankful for your feedback in the comments section 🙂
Thanks for this nice and comprehensive write-up. I am currently considering whether to set up my next Spring Boot project with coroutines. Your walk-through helps me assess the viability.
One nitpick:
For the usecase of conforming to snake case in JSON requests and responses, there is a better way than annotating each and every field. Better annotate the class:
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
Yeah, that is a good tip Franz! Thank you
Sometimes, I try to focus on a particular area in my articles/content and don’t spend too much time on things like that to avoid distracting.
About Jackson- I would go even one step further- we can configure that globally in our app. For example, for simple use cases we can make use of the application properties, e.g.: ‘spring.jackson.property-naming-strategy’ (source: https://docs.spring.io/spring-boot/appendix/application-properties/index.html)
For more fine-grained control, we could register our custom ObjectMapper, for example:
ObjectMapper()
.registerModule(KotlinModule())
(I do not recall the exact code, but basically we can configure plenty of things here)
And in general, with this approach we can have multiple mappers, we can set them for webclients, etc.