ktor-native-worker-tutorial

Part 4: Routes

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:

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

  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())
    }
}

This configuration function:

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:

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:

  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

Error Handling

The current implementation has minimal error handling:

In a production environment, you might want to add:

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

  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:

In the next part, we’ll explore how Koin dependency injection wires everything together.