-
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
138: Practice Exercise: Building a Type-Safe Builder for API Requests
We've talked about function literals with receivers, but the real magic happens when you chain them together to create a Domain Specific Language (DSL). I've always found that the best way to internalize this is by building something that actually replaces a clunky pattern. Today, we're going to build a type-safe builder for API requests. Instead of a constructor with ten nullable arguments or a chain of .setX().setY() calls, we want something that looks like a configuration block.
Defining the Request Blueprint
First, we need the actual data classes that will hold our request information. I'll keep this simple: a URL, a method, some headers, and an optional body. Notice that these are immutable data classes; the builder's job is to handle the mutation during the construction phase and then hand us back a read-only object.
data class HttpRequest(
val url: String,
val method: String,
val headers: Map<String, String>,
val body: String?
)
Structuring the Builder Logic
Now for the builder. I'll create a class that collects these values. To make it a DSL, I need a function that takes a lambda with the builder as the receiver (HttpRequestBuilder.() -> Unit). This allows the caller to use this implicitly inside the block.
class HttpRequestBuilder {
var url: String = ""
var method: String = "GET"
private val headers = mutableMapOf<String, String>()
var body: String? = null
fun header(name: String, value: String) {
headers[name] = value
}
fun build(): HttpRequest = HttpRequest(url, method, headers.toMap(), body)
}
fun httpRequest(block: HttpRequestBuilder.() -> Unit): HttpRequest {
return HttpRequestBuilder().apply(block).build()
}
The "Scope Leak" Mistake
I want to make the headers section even cleaner. Instead of calling header() repeatedly, I'll create a nested builder block specifically for headers. I'll add a headers { ... } function to the main builder.
class HeaderBuilder {
fun add(name: String, value: String) {
// I'll just implement this as a placeholder for now
}
}
// Inside HttpRequestBuilder...
fun headers(block: HeaderBuilder.() -> Unit) {
HeaderBuilder().apply(block)
}
Here is where I usually make a mistake. If I run this, I'll realize that inside the headers { ... } block, I can still call url = "..." or method = "...". Why? Because HeaderBuilder is nested inside HttpRequestBuilder's context. This "scope leak" makes the DSL confusing and error-prone. You could accidentally change the URL while you're supposed to be defining headers.
Locking Down the Scope with DslMarker
To fix this, Kotlin provides @DslMarker. This tells the compiler: "Once you are inside a receiver of this marker, you cannot access receivers of the same marker from an outer scope." I'll define a custom annotation for our API DSL.
@DslMarker
annotation class ApiDsl
@ApiDsl
class HttpRequestBuilder {
var url: String = ""
var method: String = "GET"
private val headers = mutableMapOf<String, String>()
var body: String? = null
fun header(name: String, value: String) {
headers[name] = value
}
fun headers(block: HeaderBuilder.() -> Unit) {
HeaderBuilder(this).apply(block)
}
fun build(): HttpRequest = HttpRequest(url, method, headers.toMap(), body)
}
@ApiDsl
class HeaderBuilder(private val parent: HttpRequestBuilder) {
fun add(name: String, value: String) {
parent.header(name, value)
}
}
Now, if you try to call url = "..." inside the headers { } block, the compiler will throw an error. It forces you to stay within the HeaderBuilder context, which is exactly what we want for a clean, type-safe API.
Putting it all together
With the marker in place, the final usage looks incredibly declarative. It reads almost like a JSON object, but with the full power of Kotlin's type checking.
val request = httpRequest {
url = "https://api.example.com/v1/users"
method = "POST"
body = "{ \"name\": \"Kotlin Developer\" }"
headers {
add("Content-Type", "application/json")
add("Authorization", "Bearer token123")
// url = "hack" // This would now cause a compiler error!
}
}📋 Practical Task
Exercise: Implementing a Query Parameter Builder for API Requests
Your task is to extend the HttpRequest DSL to support query parameters. Currently, the URL is just a string, but in a real API, we often need to build complex query strings (e.g., ?sort=desc&limit=10).
Requirements:
- Modify the
HttpRequestdata class to include aval queryParams: Map<String, String>. - Create a
QueryBuilderclass marked with@ApiDsl. - Add a
queryParams { ... }function toHttpRequestBuilderthat utilizes theQueryBuilder. - Ensure that
QueryBuilderhas anadd(key: String, value: String)method that updates the parent builder. - Verify that you cannot access
HttpRequestBuilderproperties (likemethod) from inside thequeryParamsblock.
Goal: You should be able to write a request like this:
val request = httpRequest {
url = "https://api.example.com/search"
queryParams {
add("q", "kotlin dsl")
add("page", "1")
}
}There are no comments for now.