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/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:
- Defines a
Book
data 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!