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:

// 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:

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:

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:

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:

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:

// 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:

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:

@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:

@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:

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

💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!