Skip to main content

Kotlin HTTP Routing

Introduction

HTTP routing is a fundamental concept in backend development that determines how an application responds to client requests at specific URL paths (endpoints). In Kotlin backend development, routing provides a way to map HTTP requests to specific handler functions based on the URL path and HTTP method (GET, POST, PUT, DELETE, etc.).

Effective routing is essential for creating well-organized, maintainable APIs and web applications. This guide will introduce you to HTTP routing in Kotlin, primarily focusing on two popular frameworks: Ktor (Kotlin's native web framework) and Spring Boot.

Basic Concepts of HTTP Routing

Before diving into implementation details, let's understand the key concepts:

  1. Routes: URL patterns that define endpoints in your application
  2. HTTP Methods: GET, POST, PUT, DELETE, PATCH, etc., specifying the action to perform
  3. Handlers: Functions executed when a specific route is accessed
  4. Parameters: Data extracted from the URL path, query string, or request body
  5. Middleware: Functions that process requests before or after a handler

Routing with Ktor

Ktor is a lightweight framework built by JetBrains specifically for Kotlin. It offers a DSL (Domain-Specific Language) for defining routes in a concise and expressive way.

Setting Up Ktor

First, add Ktor dependencies to your project:

kotlin
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-server-core:2.3.4")
implementation("io.ktor:ktor-server-netty:2.3.4")
implementation("io.ktor:ktor-server-content-negotiation:2.3.4")
implementation("io.ktor:ktor-serialization-jackson:2.3.4")
}

Basic Route Definition

Here's how to create a simple Ktor application with routes:

kotlin
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*

fun main() {
embeddedServer(Netty, port = 8080) {
routing {
// Route for GET /hello
get("/hello") {
call.respondText("Hello, World!")
}

// Route for GET /users
get("/users") {
call.respondText("List of users")
}

// Route for POST /users
post("/users") {
call.respondText("Create a user", status = HttpStatusCode.Created)
}
}
}.start(wait = true)
}

When you run this code and navigate to http://localhost:8080/hello in your browser, you'll see "Hello, World!" displayed.

Route Parameters

Routes often need to capture parameters from the URL:

kotlin
routing {
// Route with path parameter
get("/users/{id}") {
val userId = call.parameters["id"]
call.respondText("User details for user $userId")
}

// Multiple parameters
get("/products/{category}/{id}") {
val category = call.parameters["category"]
val productId = call.parameters["id"]
call.respondText("Product $productId in category $category")
}
}

Example requests:

  • GET /users/123 returns "User details for user 123"
  • GET /products/electronics/laptop returns "Product laptop in category electronics"

Nested Routes

Ktor allows you to organize your routes hierarchically:

kotlin
routing {
route("/api") {
route("/v1") {
get("/users") {
call.respondText("API v1 users")
}

post("/users") {
call.respondText("Creating user in API v1")
}
}

route("/v2") {
get("/users") {
call.respondText("API v2 users")
}
}
}
}

This creates endpoints like /api/v1/users and /api/v2/users.

Query Parameters

You can also capture query parameters:

kotlin
get("/search") {
val query = call.request.queryParameters["q"] ?: "empty"
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1

call.respondText("Searching for '$query' on page $page")
}

For a request to /search?q=kotlin&page=2, this would respond with "Searching for 'kotlin' on page 2".

Routing with Spring Boot

Spring Boot is another popular choice for Kotlin backend development, offering a mature ecosystem with extensive features.

Setting Up Spring Boot

Add these dependencies to your project:

kotlin
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:3.1.3")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
}

Basic Route Definition

In Spring Boot, routes are defined using controller classes:

kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.*

@SpringBootApplication
class Application

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

@RestController
class UserController {

@GetMapping("/hello")
fun hello(): String {
return "Hello, World!"
}

@GetMapping("/users")
fun getUsers(): String {
return "List of users"
}

@PostMapping("/users")
fun createUser(): String {
return "Create a user"
}
}

Route Parameters

Path variables in Spring Boot are defined using the @PathVariable annotation:

kotlin
@RestController
@RequestMapping("/api")
class ProductController {

@GetMapping("/users/{id}")
fun getUser(@PathVariable id: String): String {
return "User details for user $id"
}

@GetMapping("/products/{category}/{id}")
fun getProduct(@PathVariable category: String, @PathVariable id: String): String {
return "Product $id in category $category"
}
}

Query Parameters

Query parameters are handled using the @RequestParam annotation:

kotlin
@GetMapping("/search")
fun search(
@RequestParam(required = false, defaultValue = "empty") q: String,
@RequestParam(required = false, defaultValue = "1") page: Int
): String {
return "Searching for '$q' on page $page"
}

Real-World Example: Building a RESTful API

Let's create a more complete example of a RESTful API for a book management system using Ktor:

kotlin
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.serialization.jackson.*
import io.ktor.server.plugins.contentnegotiation.*
import kotlinx.serialization.Serializable
import java.util.concurrent.atomic.AtomicInteger

// Data model
data class Book(val id: Int, val title: String, val author: String, val year: Int)

// In-memory database
class BookRepository {
private val idCounter = AtomicInteger()
private val books = mutableListOf<Book>()

init {
// Add some sample data
addBook(Book(idCounter.incrementAndGet(), "1984", "George Orwell", 1949))
addBook(Book(idCounter.incrementAndGet(), "To Kill a Mockingbird", "Harper Lee", 1960))
}

fun getAllBooks(): List<Book> = books

fun getBookById(id: Int): Book? = books.find { it.id == id }

fun addBook(book: Book): Book {
val newBook = book.copy(id = if (book.id > 0) book.id else idCounter.incrementAndGet())
books.add(newBook)
return newBook
}

fun updateBook(id: Int, bookData: Book): Book? {
val index = books.indexOfFirst { it.id == id }
if (index == -1) return null

val updatedBook = bookData.copy(id = id)
books[index] = updatedBook
return updatedBook
}

fun deleteBook(id: Int): Boolean {
return books.removeIf { it.id == id }
}
}

fun main() {
val bookRepository = BookRepository()

embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
jackson()
}

routing {
route("/api/books") {
// GET all books
get {
call.respond(bookRepository.getAllBooks())
}

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

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

// POST - create a new book
post {
val book = call.receive<Book>()
val createdBook = bookRepository.addBook(book)
call.respond(HttpStatusCode.Created, createdBook)
}

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

val bookData = call.receive<Book>()
val updatedBook = bookRepository.updateBook(id, bookData)

if (updatedBook != null) {
call.respond(updatedBook)
} else {
call.respond(HttpStatusCode.NotFound, "Book not found")
}
}

// DELETE - delete a book
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")
}
}

// Search for books
get("/search") {
val query = call.request.queryParameters["q"]?.lowercase() ?: ""
val matchingBooks = bookRepository.getAllBooks().filter {
it.title.lowercase().contains(query) ||
it.author.lowercase().contains(query)
}

call.respond(matchingBooks)
}
}
}
}.start(wait = true)
}

This example:

  1. Defines a Book data model
  2. Implements a simple in-memory repository
  3. Creates a complete RESTful API with endpoints for:
    • Listing all books (GET /api/books)
    • Getting a specific book (GET /api/books/{id})
    • Creating a new book (POST /api/books)
    • Updating a book (PUT /api/books/{id})
    • Deleting a book (DELETE /api/books/{id})
    • Searching for books (GET /api/books/search?q=queryterm)

Best Practices for HTTP Routing in Kotlin

  1. Use Descriptive Route Names: Make your routes self-documenting by using clear, descriptive names.

    Good: /api/users/{id}/posts
    Bad: /api/u/{id}/p
  2. Follow RESTful Conventions:

    • Use proper HTTP methods for different operations
    • Structure your URLs around resources, not actions
    • Use plural nouns for collection endpoints
  3. Handle Errors Consistently: Create a standardized error response format for your API.

  4. Validate Input: Always validate and sanitize input data before processing.

  5. Use HTTPS: In production, always serve your API over HTTPS.

  6. Add Versioning: Consider adding API versioning from the start:

    /api/v1/users
    /api/v2/users
  7. Keep Controllers/Handlers Small: Focus on routing logic and delegate business logic to services.

  8. Document Your API: Consider using tools like Swagger/OpenAPI to document your endpoints.

Summary

HTTP routing is a core concept in backend development that enables you to structure your application's API in a clean, organized way. In this guide, we've covered:

  • Basic routing concepts
  • Implementing routes in Ktor and Spring Boot
  • Working with path and query parameters
  • Building a complete RESTful API
  • Best practices for HTTP routing

With Kotlin's expressive syntax and strong backend frameworks like Ktor and Spring Boot, you can create efficient, type-safe, and maintainable APIs.

Exercises

To reinforce your learning, try these exercises:

  1. Extend the book API example to include:

    • Filtering books by year range
    • Adding a category field and filtering by category
    • Pagination support
  2. Create a simple blog API with endpoints for:

    • Managing posts (CRUD operations)
    • Adding and retrieving comments for each post
    • User authentication (login/register routes)
  3. Implement the same API in both Ktor and Spring Boot, and compare the differences and similarities in the routing approaches.

Additional Resources



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