In this part, we’ll explore how to define HTTP routes in Ktor and how they integrate with the message broker to handle notification requests asynchronously.
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/routes/NotificationPayload.kt
@Serializable
data class NotificationPayload(
val title: String,
val body: String,
val token: String,
)
This data class represents the expected JSON payload for notification requests. The @Serializable annotation enables
automatic JSON serialization/deserialization with Kotlinx Serialization.
Expected JSON format:
{
"title": "Notification Title",
"body": "Notification message body",
"token": "FCM device token"
}
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/routes/Routes.kt
data class RoutesDependencies(
val messageBroker: MessageBroker,
)
This pattern follows the Dependency Injection principle by explicitly declaring what dependencies the routes need. Instead of using global state or service locators within route handlers, all dependencies are passed as a single parameter.
Benefits:
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/routes/Routes.kt
fun Route.registerRoutes(dependencies: RoutesDependencies) = with(dependencies) {
post<NotificationPayload>("/api/notifications") { payload ->
val event = SendNotificationEvent(
title = payload.title,
body = payload.body,
token = payload.token,
)
messageBroker.publish(
Constants.RABBITMQ_EXCHANGE, Constants.RABBITMQ_ROUTING_KEY,
Serialization.json.encodeToString(event),
)
call.respond(HttpStatusCode.OK)
}
}
Route.registerRoutes() is an extension function on Ktor’s Routewith(dependencies) creates a scope where dependency properties are directly accessiblemessageBroker without prefixingpost<NotificationPayload>("/api/notifications") defines a POST endpointNotificationPayloadSendNotificationEventSerialization.json.encodeToString()File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Routing.kt
fun Application.configureRouting() {
routing {
registerRoutes(get())
}
}
This configuration function:
Applicationrouting {} to set up routingregisterRoutes() with dependencies from Koin using get()RoutesDependenciesFile: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Serialization.kt
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Serialization.json)
}
}
This enables automatic JSON handling:
Serialization.json instance for consistencyThe ContentNegotiation plugin makes the type-safe post<NotificationPayload> syntax possible.
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/Application.kt
suspend fun Application.module() {
configureKoin()
configureMessageBroker()
configureSerialization()
configureRouting()
}
Configuration order matters:
configureKoin(): Set up dependency injection firstconfigureMessageBroker(): Initialize message brokerconfigureSerialization(): Enable JSON handlingconfigureRouting(): Register routes (depends on all above)Here’s what happens when a notification request is received:
1. HTTP POST → /api/notifications
2. Content Negotiation deserializes JSON → NotificationPayload
3. Route handler creates SendNotificationEvent
4. Event serialized to JSON
5. Message published to RabbitMQ
6. HTTP 200 OK returned to client
7. (Async) RabbitMQ consumer receives message
8. (Async) SendNotificationHandler processes event
9. (Async) NotificationService sends FCM notification
The current implementation has minimal error handling:
In a production environment, you might want to add:
Using curl:
curl -X POST http://localhost:8080/api/notifications \
-H "Content-Type: application/json" \
-d '{
"token": "fcm-device-token-here",
"title": "Hello World",
"body": "This is a test notification"
}'
Response:
HTTP/1.1 200 OK
The routing layer demonstrates:
In the next part, we’ll explore how Koin dependency injection wires everything together.