Skip to main content

Kotlin REST API

REST (Representational State Transfer) APIs have become the standard for building web services. In this tutorial, we'll explore how to build robust and efficient REST APIs using Kotlin - a modern, concise, and safe programming language that runs on the JVM.

Introduction to REST APIs with Kotlin

REST is an architectural style for designing networked applications. RESTful services allow different systems to communicate over HTTP in a stateless manner, using standard operations like GET, POST, PUT, and DELETE.

Kotlin offers several advantages for building REST APIs:

  • Concise syntax: Fewer lines of code compared to Java
  • Null safety: Reduces null pointer exceptions
  • Coroutine support: Easy to write non-blocking, asynchronous code
  • Interoperability: Works seamlessly with existing Java libraries

In this guide, we'll build a simple REST API for a book management system using popular Kotlin frameworks.

Choosing a Framework for Kotlin REST APIs

Several frameworks work well with Kotlin for building REST APIs:

  1. Spring Boot - Popular, comprehensive framework with excellent Kotlin support
  2. Ktor - Lightweight framework built by JetBrains specifically for Kotlin
  3. Micronaut - Modern, cloud-native framework with low memory footprint
  4. Javalin - Simple, lightweight web framework
  5. Quarkus - Kubernetes-native Java framework optimized for GraalVM

We'll focus on Spring Boot and Ktor, the two most common choices.

Building a REST API with Spring Boot and Kotlin

Setting Up Your Project

First, let's create a new Spring Boot project with Kotlin:

  1. Visit Spring Initializer
  2. Select:
    • Kotlin as language
    • Gradle with Kotlin DSL
    • Spring Boot version (latest stable)
    • Dependencies: Spring Web, Spring Data JPA, H2 Database

Or add these dependencies to your build.gradle.kts:

kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Creating the Data Model

Let's create a simple Book entity:

kotlin
package com.example.kotlinrestapi.model

import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id

@Entity
data class Book(
@Id @GeneratedValue
val id: Long? = null,
val title: String,
val author: String,
val year: Int,
val isbn: String
)

Creating the Repository

Next, let's create a repository interface to manage database operations:

kotlin
package com.example.kotlinrestapi.repository

import com.example.kotlinrestapi.model.Book
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface BookRepository : JpaRepository<Book, Long>

Building the REST Controller

Now let's create our REST endpoints:

kotlin
package com.example.kotlinrestapi.controller

import com.example.kotlinrestapi.model.Book
import com.example.kotlinrestapi.repository.BookRepository
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.util.*

@RestController
@RequestMapping("/api/books")
class BookController(private val bookRepository: BookRepository) {

@GetMapping
fun getAllBooks(): List<Book> = bookRepository.findAll()

@GetMapping("/{id}")
fun getBookById(@PathVariable id: Long): ResponseEntity<Book> {
return bookRepository.findById(id).map { book ->
ResponseEntity.ok(book)
}.orElse(ResponseEntity.notFound().build())
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createBook(@RequestBody book: Book): Book = bookRepository.save(book)

@PutMapping("/{id}")
fun updateBook(@PathVariable id: Long, @RequestBody book: Book): ResponseEntity<Book> {
return if (bookRepository.existsById(id)) {
bookRepository.save(
Book(
id = id,
title = book.title,
author = book.author,
year = book.year,
isbn = book.isbn
)
)
ResponseEntity.ok(book)
} else {
ResponseEntity.notFound().build()
}
}

@DeleteMapping("/{id}")
fun deleteBook(@PathVariable id: Long): ResponseEntity<Void> {
return if (bookRepository.existsById(id)) {
bookRepository.deleteById(id)
ResponseEntity.noContent().build()
} else {
ResponseEntity.notFound().build()
}
}
}

Running the Application

Create a main application class:

kotlin
package com.example.kotlinrestapi

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class KotlinRestApiApplication

fun main(args: Array<String>) {
runApplication<KotlinRestApiApplication>(*args)
}

Testing the Spring Boot REST API

Once your application is running, you can test it using tools like cURL, Postman, or HTTPie.

Example: Create a book (POST request)

bash
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{"title":"Kotlin in Action","author":"Dmitry Jemerov","year":2017,"isbn":"9781617293290"}'

Response:

json
{"id":1,"title":"Kotlin in Action","author":"Dmitry Jemerov","year":2017,"isbn":"9781617293290"}

Example: Get all books (GET request)

bash
curl http://localhost:8080/api/books

Response:

json
[{"id":1,"title":"Kotlin in Action","author":"Dmitry Jemerov","year":2017,"isbn":"9781617293290"}]

Building a REST API with Ktor

Ktor is a lightweight framework built by JetBrains specifically for Kotlin. It's designed from the ground up with coroutines in mind.

Setting Up Your Ktor Project

Add these dependencies to your build.gradle.kts:

kotlin
val ktor_version = "2.3.0"
val exposed_version = "0.41.1"
val h2_version = "2.1.214"

dependencies {
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-jackson:$ktor_version")

implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("com.h2database:h2:$h2_version")

testImplementation("io.ktor:ktor-server-test-host:$ktor_version")
}

Creating the Data Model

With Ktor, we'll use Exposed (a Kotlin SQL library) for our database operations:

kotlin
package com.example.data

import org.jetbrains.exposed.sql.Table

object Books : Table() {
val id = integer("id").autoIncrement()
val title = varchar("title", 255)
val author = varchar("author", 255)
val year = integer("year")
val isbn = varchar("isbn", 20)

override val primaryKey = PrimaryKey(id)
}

data class Book(
val id: Int? = null,
val title: String,
val author: String,
val year: Int,
val isbn: String
)

Database Setup

Let's configure our database:

kotlin
package com.example.database

import com.example.data.Books
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction

object DatabaseFactory {
fun init() {
val driverClassName = "org.h2.Driver"
val jdbcURL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
val database = Database.connect(jdbcURL, driverClassName)

transaction(database) {
SchemaUtils.create(Books)
}
}

suspend fun <T> dbQuery(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
}

Creating a Repository

Now, let's create a repository to handle book operations:

kotlin
package com.example.repository

import com.example.data.Book
import com.example.data.Books
import com.example.database.DatabaseFactory.dbQuery
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq

class BookRepository {
suspend fun getAllBooks(): List<Book> = dbQuery {
Books.selectAll().map { toBook(it) }
}

suspend fun getBook(id: Int): Book? = dbQuery {
Books.select { Books.id eq id }
.mapNotNull { toBook(it) }
.singleOrNull()
}

suspend fun addBook(book: Book): Book {
var key = 0
dbQuery {
key = (Books.insert {
it[title] = book.title
it[author] = book.author
it[year] = book.year
it[isbn] = book.isbn
} get Books.id)
}
return book.copy(id = key)
}

suspend fun updateBook(id: Int, book: Book): Boolean = dbQuery {
Books.update({ Books.id eq id }) {
it[title] = book.title
it[author] = book.author
it[year] = book.year
it[isbn] = book.isbn
} > 0
}

suspend fun deleteBook(id: Int): Boolean = dbQuery {
Books.deleteWhere { Books.id eq id } > 0
}

private fun toBook(row: ResultRow): Book =
Book(
id = row[Books.id],
title = row[Books.title],
author = row[Books.author],
year = row[Books.year],
isbn = row[Books.isbn]
)
}

Setting Up Routes

Now let's create our REST endpoints:

kotlin
package com.example.routes

import com.example.data.Book
import com.example.repository.BookRepository
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.bookRoutes(bookRepository: BookRepository) {
route("/api/books") {
get {
call.respond(bookRepository.getAllBooks())
}

get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID format")
return@get
}

val book = bookRepository.getBook(id)
if (book == null) {
call.respond(HttpStatusCode.NotFound, "Book not found")
} else {
call.respond(book)
}
}

post {
val book = call.receive<Book>()
call.respond(HttpStatusCode.Created, bookRepository.addBook(book))
}

put("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID format")
return@put
}

val book = call.receive<Book>()
val updated = bookRepository.updateBook(id, book)
if (updated) {
call.respond(HttpStatusCode.OK, "Book updated")
} else {
call.respond(HttpStatusCode.NotFound, "Book not found")
}
}

delete("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID format")
return@delete
}

val deleted = bookRepository.deleteBook(id)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, "Book not found")
}
}
}
}

Main Application

Finally, let's set up our main application:

kotlin
package com.example

import com.example.database.DatabaseFactory
import com.example.repository.BookRepository
import com.example.routes.bookRoutes
import io.ktor.serialization.jackson.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.routing.*

fun main() {
embeddedServer(Netty, port = 8080) {
module()
}.start(wait = true)
}

fun Application.module() {
// Initialize database
DatabaseFactory.init()
val bookRepository = BookRepository()

// Install plugins
install(ContentNegotiation) {
jackson()
}

// Configure routing
routing {
bookRoutes(bookRepository)
}
}

Testing the Ktor REST API

You can test the Ktor API using the same tools and commands as shown for Spring Boot.

Best Practices for Kotlin REST APIs

Regardless of which framework you choose, here are some best practices:

  1. Use data classes for DTOs: Kotlin's data classes are perfect for representing data transfer objects
kotlin
data class BookRequest(val title: String, val author: String, val year: Int, val isbn: String)
data class BookResponse(val id: Long, val title: String, val author: String, val year: Int, val isbn: String)
  1. Implement proper error handling:
kotlin
@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ResponseEntity<ErrorResponse> {
val errorResponse = ErrorResponse(
status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
error = "Internal Server Error",
message = e.message ?: "Unknown error"
)
return ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR)
}

data class ErrorResponse(val status: Int, val error: String, val message: String)
  1. Add validation: Use Bean Validation API with Spring or Ktor's validation features

  2. Implement pagination: For endpoints that might return large datasets

kotlin
@GetMapping
fun getAllBooks(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int
): Page<Book> {
return bookRepository.findAll(PageRequest.of(page, size))
}
  1. Add API documentation: Use Swagger/OpenAPI with tools like SpringDoc or Ktor OpenAPI Generator

  2. Implement HATEOAS: For more sophisticated RESTful APIs

  3. Use coroutines for asynchronous operations:

kotlin
@GetMapping
suspend fun getAllBooks(): List<Book> = withContext(Dispatchers.IO) {
bookRepository.findAll()
}

Real-World Example: A Weather API Client

Let's create a simple weather API client that fetches data from a public API:

kotlin
package com.example.weather

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import org.springframework.web.util.UriComponentsBuilder

@Service
class WeatherService(private val restTemplate: RestTemplate) {

private val apiKey = "your-api-key"
private val baseUrl = "https://api.weatherapi.com/v1"

suspend fun getCurrentWeather(city: String): WeatherResponse = withContext(Dispatchers.IO) {
val uri = UriComponentsBuilder.fromHttpUrl("$baseUrl/current.json")
.queryParam("key", apiKey)
.queryParam("q", city)
.build()
.toUri()

restTemplate.getForObject(uri, WeatherResponse::class.java)
?: throw RuntimeException("Failed to fetch weather data")
}
}

data class WeatherResponse(
val location: Location,
val current: Current
)

data class Location(
val name: String,
val region: String,
val country: String,
val lat: Double,
val lon: Double
)

data class Current(
val temp_c: Double,
val temp_f: Double,
val condition: Condition,
val humidity: Int,
val wind_kph: Double
)

data class Condition(
val text: String,
val icon: String
)

And a controller to expose the weather API:

kotlin
@RestController
@RequestMapping("/api/weather")
class WeatherController(private val weatherService: WeatherService) {

@GetMapping("/{city}")
suspend fun getWeather(@PathVariable city: String): WeatherResponse {
return weatherService.getCurrentWeather(city)
}
}

Summary

In this guide, we've explored how to build REST APIs in Kotlin using two popular frameworks: Spring Boot and Ktor. We've covered:

  • Setting up a Kotlin REST API project
  • Creating data models and repositories
  • Implementing REST controllers/routes
  • Adding CRUD operations for a book management system
  • Best practices for Kotlin REST APIs
  • A real-world example of consuming a third-party REST API

Both Spring Boot and Ktor provide excellent solutions for building REST APIs with Kotlin. Spring Boot offers a comprehensive ecosystem with many built-in features, while Ktor provides a lightweight, coroutine-first approach designed specifically for Kotlin.

Additional Resources

Exercises

  1. Extend the book API to include search functionality (by author, title, etc.)
  2. Add input validation to both Spring Boot and Ktor examples
  3. Implement pagination for the "get all books" endpoint
  4. Create a client to consume your REST API from another Kotlin application
  5. Add user authentication to secure your API endpoints
  6. Implement versioning for your API (e.g., /api/v1/books)
  7. Create a simple front-end application that communicates with your API

With these exercises, you'll gain hands-on experience building and extending REST APIs with Kotlin!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)