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:
- Routes: URL patterns that define endpoints in your application
- HTTP Methods: GET, POST, PUT, DELETE, PATCH, etc., specifying the action to perform
- Handlers: Functions executed when a specific route is accessed
- Parameters: Data extracted from the URL path, query string, or request body
- 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/123returns "User details for user 123"
- GET /products/electronics/laptopreturns "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:
- Defines a Bookdata model
- Implements a simple in-memory repository
- 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)
 
- Listing all books (
Best Practices for HTTP Routing in Kotlin
- 
Use Descriptive Route Names: Make your routes self-documenting by using clear, descriptive names. Good: /api/users/{id}/posts
 Bad: /api/u/{id}/p
- 
Follow RESTful Conventions: - Use proper HTTP methods for different operations
- Structure your URLs around resources, not actions
- Use plural nouns for collection endpoints
 
- 
Handle Errors Consistently: Create a standardized error response format for your API. 
- 
Validate Input: Always validate and sanitize input data before processing. 
- 
Use HTTPS: In production, always serve your API over HTTPS. 
- 
Add Versioning: Consider adding API versioning from the start: /api/v1/users
 /api/v2/users
- 
Keep Controllers/Handlers Small: Focus on routing logic and delegate business logic to services. 
- 
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:
- 
Extend the book API example to include: - Filtering books by year range
- Adding a category field and filtering by category
- Pagination support
 
- 
Create a simple blog API with endpoints for: - Managing posts (CRUD operations)
- Adding and retrieving comments for each post
- User authentication (login/register routes)
 
- 
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!