DEV Community

Nathan Fallet
Nathan Fallet

Posted on

Part 4: Routes - Ktor Native Worker Tutorial

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,
)
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Routes Dependencies

File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/routes/Routes.kt

data class RoutesDependencies(
    val messageBroker: MessageBroker,
)
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Route

  1. Extension Function:

    • Route.registerRoutes() is an extension function on Ktor's Route
    • Allows modular route registration
    • Can be called from the routing configuration
  2. Dependency Scope:

    • with(dependencies) creates a scope where dependency properties are directly accessible
    • Provides clean access to messageBroker without prefixing
  3. 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
  4. Event Creation:

    • Maps the HTTP payload to a SendNotificationEvent
    • This separation allows different internal/external representations
    • The event is what gets published to RabbitMQ
  5. 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
  6. 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())
    }
}
Enter fullscreen mode Exit fullscreen mode

This configuration function:

  • Is an extension on Application
  • Calls routing {} to set up routing
  • Calls registerRoutes() with dependencies from Koin using get()
  • 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

This enables automatic JSON handling:

  • Deserializes incoming JSON to Kotlin objects (request bodies)
  • Serializes Kotlin objects to JSON (responses)
  • Uses the same Serialization.json instance 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()
}
Enter fullscreen mode Exit fullscreen mode

Configuration order matters:

  1. configureKoin(): Set up dependency injection first
  2. configureMessageBroker(): Initialize message broker
  3. configureSerialization(): Enable JSON handling
  4. 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
Enter fullscreen mode Exit fullscreen mode

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"
  }'
Enter fullscreen mode Exit fullscreen mode

Response:

HTTP/1.1 200 OK
Enter fullscreen mode Exit fullscreen mode

Benefits of This Architecture

  1. Async Processing:

    • HTTP response is immediate
    • Notification sending happens in the background
    • Better user experience and API performance
  2. Decoupling:

    • HTTP layer is separate from notification logic
    • Can change notification implementation without touching routes
    • Message broker provides a clear boundary
  3. Scalability:

    • Can scale HTTP servers independently from workers
    • Queue provides buffering during traffic spikes
    • Workers can be added/removed dynamically
  4. Reliability:

    • Messages are persisted in RabbitMQ
    • HTTP failures don't lose notification requests
    • Can retry failed notifications
  5. 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)