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
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)