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.
Route Payload
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"
}
Routes Dependencies
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:
- Clear visibility of route dependencies
- Easy to test (can mock dependencies)
- Type-safe dependency access
- Clean separation of concerns
Route Registration
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)
}
}
Understanding the Route
-
Extension Function:
-
Route.registerRoutes()is an extension function on Ktor'sRoute - Allows modular route registration
- Can be called from the routing configuration
-
-
Dependency Scope:
-
with(dependencies)creates a scope where dependency properties are directly accessible - Provides clean access to
messageBrokerwithout prefixing
-
-
Type-Safe POST Handler:
-
post<NotificationPayload>("/api/notifications")defines a POST endpoint - Ktor automatically deserializes the request body to
NotificationPayload - Type safety ensures compile-time checking of payload structure
-
-
Event Creation:
- Maps the HTTP payload to a
SendNotificationEvent - This separation allows different internal/external representations
- The event is what gets published to RabbitMQ
- Maps the HTTP payload to a
-
Message Publishing:
- Serializes the event to JSON using
Serialization.json.encodeToString() - Publishes to the RabbitMQ exchange with the routing key
- Message will be queued and processed asynchronously
- Serializes the event to JSON using
-
Response:
- Returns HTTP 200 OK immediately
- Client doesn't wait for the notification to be sent
- Improves response time and user experience
Integration with Ktor Application
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Routing.kt
fun Application.configureRouting() {
routing {
registerRoutes(get())
}
}
This configuration function:
- Is an extension on
Application - Calls
routing {}to set up routing - Calls
registerRoutes()with dependencies from Koin usingget() - Ktor's Koin integration automatically resolves the
RoutesDependencies
Content Negotiation Configuration
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Serialization.kt
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Serialization.json)
}
}
This enables automatic JSON handling:
- Deserializes incoming JSON to Kotlin objects (request bodies)
- Serializes Kotlin objects to JSON (responses)
- Uses the same
Serialization.jsoninstance for consistency
The ContentNegotiation plugin makes the type-safe post<NotificationPayload> syntax possible.
Application Module Setup
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 first -
configureMessageBroker(): Initialize message broker -
configureSerialization(): Enable JSON handling -
configureRouting(): Register routes (depends on all above)
Request Flow
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
Error Handling
The current implementation has minimal error handling:
- Ktor automatically returns 400 Bad Request for invalid JSON
- Missing required fields result in serialization errors
- Type mismatches are caught during deserialization
In a production environment, you might want to add:
- Validation of FCM token format
- Rate limiting
- Authentication/authorization
- Custom error responses
- Logging
Example Request
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
Benefits of This Architecture
-
Async Processing:
- HTTP response is immediate
- Notification sending happens in the background
- Better user experience and API performance
-
Decoupling:
- HTTP layer is separate from notification logic
- Can change notification implementation without touching routes
- Message broker provides a clear boundary
-
Scalability:
- Can scale HTTP servers independently from workers
- Queue provides buffering during traffic spikes
- Workers can be added/removed dynamically
-
Reliability:
- Messages are persisted in RabbitMQ
- HTTP failures don't lose notification requests
- Can retry failed notifications
-
Clean Code:
- Type-safe route handlers
- Explicit dependencies
- Clear separation of concerns
- Easy to test
Summary
The routing layer demonstrates:
- Type-safe HTTP endpoint definition with Ktor
- Automatic JSON serialization/deserialization
- Dependency injection for route handlers
- Asynchronous message publishing
- Clean separation between HTTP and business logic
In the next part, we'll explore how Koin dependency injection wires everything together.
Top comments (0)