Building a REST Web Service Using Kotlin

Femi Adigun profile picture

Femi Adigun

Senior Software Engineer & Coach

Updated 01-10-2023

Introduction

Kotlin has rapidly grown in popularity as a language for backend development, particularly for building RESTful web services. Its concise syntax, null safety, and full interoperability with Java make it an excellent choice for modern server-side applications.

In this tutorial, we will walk through the process of building a robust REST API using Kotlin and Spring Boot. By the end, you will have a fully functional web service that follows best practices for API development.

Why Choose Kotlin for Your REST API?

Concise Syntax

Kotlin reduces boilerplate code significantly compared to Java, making your codebase more maintainable.

Null Safety

The type system distinguishes between nullable and non-nullable types, preventing the infamous NullPointerException.

Extension Functions

Add functionality to existing classes without inheritance, perfect for enhancing Spring Boot's capabilities.

Coroutines Support

Built-in support for asynchronous programming with coroutines, making it easier to handle concurrent operations.

Prerequisites

Before we start, make sure you have the following installed:

  • JDK 11 or newer
  • Gradle or Maven (we will use Gradle in this tutorial)
  • An IDE with Kotlin support (IntelliJ IDEA recommended)
  • Basic knowledge of Spring Framework concepts

Setting Up Your Project

Using Spring Initializr

The easiest way to set up a Kotlin Spring Boot project is to use Spring Initializr:

  1. Go to https://start.spring.io
  2. Choose "Gradle Project" with "Kotlin" language
  3. Select Spring Boot version 2.7.x or newer
  4. Add dependencies: Spring Web, Spring Data JPA, H2 Database, Validation
  5. Generate and download the project

Project Structure

After extracting the ZIP file, you will have a basic project structure. Here is how we will organize our code:

src/main/kotlin/com/example/api/
├── KotlinRestApiApplication.kt      # Main application class
├── controller/                      # REST controllers
│   └── ProductController.kt
├── model/                           # Domain models
│   └── Product.kt
├── repository/                      # Data access layer
│   └── ProductRepository.kt
├── service/                         # Business logic
│   └── ProductService.kt
└── exception/                       # Custom exceptions & handlers
    └── GlobalExceptionHandler.kt

Defining Your Data Model

Let us start by creating a simple Product model. In Kotlin, we can use a data class, which automatically provides equals(), hashCode(), toString() and copy() methods:

// src/main/kotlin/com/example/api/model/Product.kt
package com.example.api.model

import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Positive
import java.math.BigDecimal

@Entity
data class Product(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    
    @get:NotBlank(message = "Name is required")
    val name: String,
    
    val description: String? = null,
    
    @get:Positive(message = "Price must be positive")
    val price: BigDecimal,
    
    @get:Positive(message = "Quantity must be positive")
    val quantity: Int
)

Notice how we've used Kotlin's nullable types (String?) and default parameters to make our code more expressive. We&apo;ve also added Bean Validation annotations to enforce validation rules.

Creating the Repository Layer

Next, let us create a repository interface using Spring Data JPA. The power of Spring Data is that we just need to define the interface, and Spring will provide the implementation at runtime:

// src/main/kotlin/com/example/api/repository/ProductRepository.kt
package com.example.api.repository

import com.example.api.model.Product
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface ProductRepository : JpaRepository<Product, Long> {
    // Spring Data will automatically implement basic CRUD operations
    
    // Custom query methods
    fun findByNameContainingIgnoreCase(name: String): List<Product>
}

By extending JpaRepository, we automatically get methods for CRUD operations like save(), findById(), findAll(), and delete(). We have also added a custom query method that Spring Data will implement based on the method name.

Implementing the Service Layer

Now let us create a service class that will contain our business logic:

// src/main/kotlin/com/example/api/service/ProductService.kt
package com.example.api.service

import com.example.api.model.Product
import com.example.api.repository.ProductRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import javax.persistence.EntityNotFoundException

@Service
class ProductService(private val productRepository: ProductRepository) {
    
    fun getAllProducts(): List<Product> = productRepository.findAll()
    
    fun getProductById(id: Long): Product =
        productRepository.findById(id).orElseThrow { 
            EntityNotFoundException("Product not found with id: $id") 
        }
    
    fun searchProducts(query: String): List<Product> =
        productRepository.findByNameContainingIgnoreCase(query)
    
    @Transactional
    fun createProduct(product: Product): Product = productRepository.save(product)
    
    @Transactional
    fun updateProduct(id: Long, productDetails: Product): Product {
        val existingProduct = getProductById(id)
        
        // Use Kotlin's copy() function to create an updated copy
        val updatedProduct = existingProduct.copy(
            name = productDetails.name,
            description = productDetails.description,
            price = productDetails.price,
            quantity = productDetails.quantity
        )
        
        return productRepository.save(updatedProduct)
    }
    
    @Transactional
    fun deleteProduct(id: Long) {
        if (productRepository.existsById(id)) {
            productRepository.deleteById(id)
        } else {
            throw EntityNotFoundException("Product not found with id: $id") 
        }
    }
}

Note how we are using constructor injection with Kotlin's concise primary constructor. We are also using Kotlin's expression functions for simple methods, making the code very readable.

Building the REST Controller

Now let us create the controller class that will expose our REST endpoints:

// src/main/kotlin/com/example/api/controller/ProductController.kt
package com.example.api.controller

import com.example.api.model.Product
import com.example.api.service.ProductService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import javax.validation.Valid

@RestController
@RequestMapping("/api/products")
class ProductController(private val productService: ProductService) {
    
    @GetMapping
    fun getAllProducts(): ResponseEntity<List<Product>> =
        ResponseEntity.ok(productService.getAllProducts())
    
    @GetMapping("/{id}")
    fun getProductById(@PathVariable id: Long): ResponseEntity<Product> =
        ResponseEntity.ok(productService.getProductById(id))
    
    @GetMapping("/search")
    fun searchProducts(@RequestParam query: String): ResponseEntity<List<Product>> =
        ResponseEntity.ok(productService.searchProducts(query))
    
    @PostMapping
    fun createProduct(@Valid @RequestBody product: Product): ResponseEntity<Product> =
        ResponseEntity.status(HttpStatus.CREATED).body(productService.createProduct(product))
    
    @PutMapping("/{id}")
    fun updateProduct(
        @PathVariable id: Long,
        @Valid @RequestBody productDetails: Product
    ): ResponseEntity<Product> =
        ResponseEntity.ok(productService.updateProduct(id, productDetails))
    
    @DeleteMapping("/{id}")
    fun deleteProduct(@PathVariable id: Long): ResponseEntity<Unit> {
        productService.deleteProduct(id)
        return ResponseEntity.noContent().build()
    }
}

Our controller is clean and concise, thanks to Kotlin's expression functions. Notice how we are using the @Valid annotation to trigger validation of incoming request bodies, based on the constraints we defined in our model.

Error Handling

Let us add a global exception handler to provide consistent error responses:

// src/main/kotlin/com/example/api/exception/GlobalExceptionHandler.kt
package com.example.api.exception

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import javax.persistence.EntityNotFoundException
import java.time.LocalDateTime

@RestControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(EntityNotFoundException::class)
    fun handleEntityNotFound(ex: EntityNotFoundException): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            timestamp = LocalDateTime.now(),
            status = HttpStatus.NOT_FOUND.value(),
            error = "Not Found",
            message = ex.message ?: "Entity not found"
        )
        return ResponseEntity(errorResponse, HttpStatus.NOT_FOUND)
    }
    
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationExceptions(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
        val errors = ex.bindingResult.fieldErrors.associate { it.field to it.defaultMessage }
        
        val errorResponse = ErrorResponse(
            timestamp = LocalDateTime.now(),
            status = HttpStatus.BAD_REQUEST.value(),
            error = "Validation Error",
            message = "Invalid request parameters",
            details = errors
        )
        return ResponseEntity(errorResponse, HttpStatus.BAD_REQUEST)
    }
    
    @ExceptionHandler(Exception::class)
    fun handleAllExceptions(ex: Exception): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            timestamp = LocalDateTime.now(),
            status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
            error = "Server Error",
            message = ex.message ?: "Unexpected error occurred"
        )
        return ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR)
    }
}

data class ErrorResponse(
    val timestamp: LocalDateTime,
    val status: Int,
    val error: String,
    val message: String,
    val details: Map<String, String?> = emptyMap()
)

This exception handler will catch different types of exceptions and return appropriate HTTP responses. We are using a Kotlin data class for our error response structure, which makes it easy to create and serialize.

Configuration

Let us configure our application by updating the properties file:

# src/main/resources/application.properties

# Database Configuration
spring.datasource.url=jdbc:h2:mem:productdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# Server Configuration
server.port=8080

For simplicity, we are using an H2 in-memory database. In a production environment, you would want to use a persistent database like PostgreSQL or MySQL.

Testing Your REST API

Let us write a simple test for our controller using Kotlin and JUnit 5:

// src/test/kotlin/com/example/api/controller/ProductControllerTest.kt
package com.example.api.controller

import com.example.api.model.Product
import com.example.api.service.ProductService
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.mockito.Mockito.when
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import java.math.BigDecimal

@WebMvcTest(ProductController::class)
class ProductControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc
    
    @Autowired
    private lateinit var objectMapper: ObjectMapper
    
    @MockBean
    private lateinit var productService: ProductService
    
    @Test
    fun 'should return product when getting product by id'() {
        // given
        val product = Product(
            id = 1,
            name = "Test Product",
            description = "Test Description",
            price = BigDecimal("19.99"),
            quantity = 10
        )
        
        'when'(productService.getProductById(1)).thenReturn(product)
        
        // when/then
        mockMvc.perform(get("/api/products/1"))
            .andExpect(status().isOk)
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.name").value("Test Product"))
    }
    
    @Test
    fun 'should create product and return 201 status'() {
        // given
        val productToCreate = Product(
            name = "New Product",
            description = "New Description",
            price = BigDecimal("29.99"),
            quantity = 5
        )
        
        val createdProduct = productToCreate.copy(id = 1)
        
        'when'(productService.createProduct(productToCreate)).thenReturn(createdProduct)
        
        // when/then
        mockMvc.perform(
            post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(productToCreate))
        )
            .andExpect(status().isCreated)
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.name").value("New Product"))
    }
}

In these tests, we are using Kotlin's string templates and multiline string literals to make our code more readable. We are also taking advantage of the copy() method from our data class to create modified instances.

Running Your Application

Now that we have set up our REST API, let us run it and test it manually:

./gradlew bootRun

Your application should start on port 8080. Here are some curl commands to test the API:

# Create a product
curl -X POST http://localhost:8080/api/products   -H "Content-Type: application/json"   -d '{"name":"Kotlin T-Shirt","description":"Comfortable t-shirt with Kotlin logo","price":24.99,"quantity":100}'

# Get all products
curl http://localhost:8080/api/products

# Get a specific product
curl http://localhost:8080/api/products/1

# Update a product
curl -X PUT http://localhost:8080/api/products/1   -H "Content-Type: application/json"   -d '{"name":"Kotlin T-Shirt","description":"Super comfortable t-shirt with Kotlin logo","price":29.99,"quantity":50}'

# Delete a product
curl -X DELETE http://localhost:8080/api/products/1

Adding Swagger Documentation

Let us add Swagger documentation to make our API more accessible:

First, add these dependencies to your build.gradle.kts:


                  implementation("org.springdoc:springdoc-openapi-ui:1.6.11")
                  implementation("org.springdoc:springdoc-openapi-kotlin:1.6.11")
                  

Next, let us configure Swagger by creating a configuration class:

// src/main/kotlin/com/example/api/config/OpenApiConfig.kt
package com.example.api.config

import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.info.Info
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class OpenApiConfig {
    
    @Bean
    fun customOpenAPI(): OpenAPI {
        return OpenAPI()
            .info(
                Info()
                    .title("Product Management API")
                    .description("REST API for managing products built with Kotlin and Spring Boot")
                    .version("1.0.0")
                    .contact(
                        Contact()
                            .name("Your Name")
                            .email("your.email@example.com")
                    )
            )
    }
}

Now, you can access the Swagger UI at http://localhost:8080/swagger-ui.html when your application is running.

Advanced Features

1. Using Kotlin Coroutines

If you want to make your API non-blocking, you can use Kotlin coroutines. First, add these dependencies:


                  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
                  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
                  

Then, update your service to use coroutines:

@Service
class ProductService(private val productRepository: ProductRepository) {
    
    suspend fun getAllProducts(): List<Product> =
        withContext(Dispatchers.IO) {
            productRepository.findAll()
        }
    
    // Other methods with suspend modifier...
}

2. Implementing Pagination

For large datasets, you will want to implement pagination:

@GetMapping
fun getAllProducts(
    @RequestParam(defaultValue = "0") page: Int,
    @RequestParam(defaultValue = "10") size: Int
): ResponseEntity<Page<Product>> {
    val pageable = PageRequest.of(page, size, Sort.by("name"))
    return ResponseEntity.ok(productService.getAllProducts(pageable))
}

Deploying Your API

Building a JAR File

To deploy your application, you will first need to build an executable JAR file:

./gradlew clean build

This will create a JAR file in the build/libs directory that contains your application and all its dependencies.

Containerization with Docker

For containerized deployment, create a Dockerfile in your project root:

FROM openjdk:11-jre-slim

WORKDIR /app

COPY build/libs/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

Build and run your Docker container:

# Build the image
docker build -t kotlin-rest-api .

# Run the container
docker run -p 8080:8080 kotlin-rest-api

Cloud Deployment Options

There are several options for deploying your Kotlin Spring Boot application to the cloud:

  • AWS Elastic Beanstalk: Upload your JAR file directly or use Docker deployment
  • Google Cloud Run: Great for containerized applications with automatic scaling
  • Azure App Service: Supports both JAR deployment and Docker containers
  • Heroku: Simple deployment with either buildpacks or Docker
  • Kubernetes: For more complex deployment scenarios requiring orchestration

Performance Tuning

To ensure your Kotlin REST API performs well under load, consider these optimizations:

JVM Settings

Optimize JVM memory settings using the -Xms and -Xmx flags to control heap size, and consider using the G1GC garbage collector for larger applications.

Database Optimization

Use indexing for frequently queried fields, optimize queries, and consider implementing caching with tools like Caffeine or Redis.

Connection Pooling

Configure connection pooling properly using HikariCP (included with Spring Boot) to optimize database connection management.

Asynchronous Processing

Use Kotlin coroutines for IO-bound operations and consider implementing reactive programming with Spring WebFlux for high-throughput scenarios.

Security Best Practices

Securing your REST API is critical. Here are some important security considerations:

// Add Spring Security dependency
implementation("org.springframework.boot:spring-boot-starter-security")

// Add OAuth2 Resource Server for JWT validation
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")

Implement basic security configuration:

@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
    
    override fun configure(http: HttpSecurity) {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/products/**").authenticated()
            .and()
            .httpBasic()
    }
    
    @Bean
    override fun userDetailsService(): UserDetailsService {
        // In production, use proper user store
        val user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build()
            
        return InMemoryUserDetailsManager(user)
    }
}

Additional security measures to consider:

  • Use HTTPS in production
  • Implement proper input validation
  • Add rate limiting to prevent abuse
  • Set up CORS properly
  • Use JWT for stateless authentication
  • Implement proper role-based access control

Conclusion

In this tutorial, we have built a complete REST API using Kotlin and Spring Boot. We have covered everything from setting up the project to implementing CRUD operations, error handling, testing, and documentation.

Kotlin offers many advantages for backend development, including concise syntax, null safety, and powerful features like data classes and extension functions. Combined with Spring Boot&apo;s robust architecture, it provides an excellent platform for building modern, maintainable web services.

As you continue to develop your API, consider adding more advanced features like:

  • Authentication and authorization with Spring Security
  • Rate limiting for API endpoints
  • Caching for improved performance
  • Metrics and monitoring
  • Containerization with Docker

By following the principles demonstrated in this tutorial, you will be well-equipped to build production-ready REST APIs using Kotlin and Spring Boot.