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:
- Spring Boot - Popular, comprehensive framework with excellent Kotlin support
- Ktor - Lightweight framework built by JetBrains specifically for Kotlin
- Micronaut - Modern, cloud-native framework with low memory footprint
- Javalin - Simple, lightweight web framework
- 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:
- Visit Spring Initializer
- 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
:
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:
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:
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:
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:
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)
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:
{"id":1,"title":"Kotlin in Action","author":"Dmitry Jemerov","year":2017,"isbn":"9781617293290"}
Example: Get all books (GET request)
curl http://localhost:8080/api/books
Response:
[{"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
:
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:
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:
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:
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:
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:
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:
- Use data classes for DTOs: Kotlin's data classes are perfect for representing data transfer objects
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)
- Implement proper error handling:
@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)
-
Add validation: Use Bean Validation API with Spring or Ktor's validation features
-
Implement pagination: For endpoints that might return large datasets
@GetMapping
fun getAllBooks(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int
): Page<Book> {
return bookRepository.findAll(PageRequest.of(page, size))
}
-
Add API documentation: Use Swagger/OpenAPI with tools like SpringDoc or Ktor OpenAPI Generator
-
Implement HATEOAS: For more sophisticated RESTful APIs
-
Use coroutines for asynchronous operations:
@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:
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:
@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
- Official Kotlin Documentation
- Spring Boot with Kotlin Guide
- Ktor Documentation
- RESTful API Design Best Practices
- HTTP Status Codes
Exercises
- Extend the book API to include search functionality (by author, title, etc.)
- Add input validation to both Spring Boot and Ktor examples
- Implement pagination for the "get all books" endpoint
- Create a client to consume your REST API from another Kotlin application
- Add user authentication to secure your API endpoints
- Implement versioning for your API (e.g., /api/v1/books)
- 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! :)