Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
117: Ktor Routing In Depth
Routing is where Ktor really shows its flexibility, but if you're coming from a framework like Spring, you might expect a more declarative, annotation-based approach. Ktor does things differently. It uses a DSL that essentially builds a routing tree. Once you understand that tree structure, you can do some pretty sophisticated things with your URL patterns.
To get our hands dirty, we're going to build a small API for a Movie Catalog. We aren't just doing a simple list; we want to handle specific movie lookups, nested reviews, and some filtering. I've found that the best way to master Ktor routing is to see how the nesting actually behaves in practice.
Setting up the Movie Base
First, let's start with the basic collection. I'll wrap everything in a routing block. I like to group related endpoints using the route() function. It keeps the code clean and allows us to apply specific plugins (like authentication) to a whole group of endpoints later on without repeating ourselves.
routing {
route("/movies") {
get {
call.respondText("Here is the full list of movies!")
}
}
}
Capturing IDs with Path Parameters
Now, we need to be able to fetch a single movie. In Ktor, we use curly braces to define a path parameter. I'll add this inside our /movies route. This means the actual path becomes /movies/{id}.
route("/movies") {
get {
call.respondText("Here is the full list of movies!")
}
get("{id}") {
val id = call.parameters["id"] ?: return@get call.respondText("Missing ID", status = HttpStatusCode.BadRequest)
call.respondText("Fetching details for movie: $id")
}
}
Note how I used return@get. Since we're inside a lambda, a standard return would try to return from the entire function containing the routing block, which isn't what we want. We just want to exit the current handler.
Nesting Reviews Under Movies
This is where the "tree" concept becomes powerful. A movie has reviews. It makes semantic sense for the URL to look like /movies/123/reviews. Instead of writing a flat string for that route, I'm going to nest another route block inside the {id} block.
route("/movies") {
get { /* ... list movies ... */ }
route("{id}") {
get {
val id = call.parameters["id"]
call.respondText("Details for movie $id")
}
route("/reviews") {
get {
val id = call.parameters["id"]
call.respondText("Showing reviews for movie $id")
}
}
}
}
Because the /reviews route is nested inside {id}, it automatically inherits that path parameter. I can access id inside the reviews handler without having to redefine it.
The Shadowing Trap
I've made this mistake more times than I'd like to admit. Let's say I want a special endpoint to get the "featured" movie of the week. I'll add a route for /movies/featured.
route("/movies") {
get("{id}") {
call.respondText("Movie ID: ${call.parameters["id"]}")
}
get("featured") {
call.respondText("This is the featured movie!")
}
}
If I run this and hit /movies/featured, Ktor is going to tell me "Movie ID: featured". Why? Because the {id} route is a wildcard that matches anything that comes after /movies/. Since I defined it first, Ktor matches it and never even looks at the featured route.
To fix this, I have to move the most specific routes to the top. The routing tree is evaluated in the order it's defined. I'll swap them:
route("/movies") {
get("featured") {
call.respondText("This is the featured movie!")
}
get("{id}") {
call.respondText("Movie ID: ${call.parameters["id"]}")
}
}
Now, Ktor checks if the path is exactly "featured" first. If it isn't, it falls back to the {id} parameter. Much better.
Filtering with Query Parameters
Finally, let's handle filtering. Unlike path parameters, query parameters (the stuff after the ? in a URL) aren't part of the route definition itself. They are handled inside the handler using call.request.queryParameters.
I'll update our main movies list to filter by genre if the user provides one:
get {
val genre = call.request.queryParameters["genre"]
if (genre != null) {
call.respondText("Filtering movies by genre: $genre")
} else {
call.respondText("Here is the full list of movies!")
}
}
Now, a request to /movies?genre=SciFi works perfectly, while /movies still gives the full list.
📋 Practical Task
Build a Nested "Actor" API
Expand the Movie Catalog project by adding a new routing section for Actors. Your implementation must meet the following requirements:
- Create a base route
/actors. - Implement a
GETrequest to/actors/topthat returns a hardcoded string listing the top actors. - Implement a
GETrequest to/actors/{name}that returns the name of the actor from the path parameter. - Inside the
{name}route, nest a further route/movies. AGETrequest to/actors/{name}/moviesshould return a string stating: "Listing all movies for actor [name]".
Crucial: Ensure your routes are ordered correctly so that the /top endpoint is not "swallowed" by the /{name} parameter.
There are no comments for now.