Getting started

Add Connect-Ktor beside an existing Ktor server and call a generated handler.

Getting started

The goal is a unary Connect RPC on a Ktor server that can still host REST routes. Prerequisites match a typical Connect-Kotlin workspace: JDK 21-friendly tooling, Gradle, and the Buf CLI.

This guide is server-side only. If you need a Connect client first, follow the official Connect-Kotlin getting started guide, then come back here to expose the matching Ktor handlers. For protocol details (codecs, errors, Connect GET), see the Connect protocol specification.

Add the library

dependencies {
    implementation("io.github.ichizero:connect-ktor:0.4.1")
}

Pin to the current release on Maven Central.

Install the generator

On macOS or Linux:

brew install ichizero/tap/protoc-gen-connect-ktor

Or download a binary from GitHub Releases and put it on PATH.

Generate handlers

Point Buf at the local plugin alongside the usual Java/Kotlin and Connect-Kotlin plugins:

version: v2
clean: true
managed:
  enabled: true
plugins:
  - remote: buf.build/protocolbuffers/java
    out: path/to/code
  - remote: buf.build/protocolbuffers/kotlin
    out: path/to/code
  - remote: buf.build/connectrpc/kotlin
    out: path/to/code
  - local: protoc-gen-connect-ktor
    out: path/to/code
buf generate

For a service like Eliza's Say RPC, the plugin emits a handler interface and a Route extension that wires handle / handleClientStream / handleServerStream.

Implement the handler

object ElizaServiceHandler : ElizaServiceHandlerInterface {
    override suspend fun say(
        request: SayRequest,
        call: ApplicationCall,
    ): ResponseMessage<SayResponse> = ResponseMessage.Success(
        sayResponse { sentence = request.sentence },
        emptyMap(),
        emptyMap(),
    )
}

Unary methods take a single request. Client-streaming methods take a cold Flow<Req> and still return one ResponseMessage. Server-streaming methods take a single request and return a cold Flow<Res>.

Register beside REST

fun main() {
    embeddedServer(CIO, port = 8080) {
        install(Resources)
        routing {
            get("/health") {
                call.respondText("ok")
            }
            install(ContentNegotiation) {
                connectJson()
            }
            elizaService(ElizaServiceHandler)
        }
    }.start(wait = true)
}

/health stays ordinary Ktor. ElizaService/Say becomes a Connect procedure on the same application. That coexistence is the migration model: move RPCs when ready, leave the rest alone.

Call it from a Connect client

Any Connect client that targets your host works. On the JVM, use the official Connect-Kotlin client; from React or other TypeScript apps, use Connect-ES. Buf Studio is fine for ad-hoc checks.

Point the client at http://localhost:8080 (or your deployed base URL) and invoke Say as usual. Client configuration details live in those official guides, not here.

For a per-call deadline, send Connect-Timeout-Ms with a positive number of milliseconds. For example:

curl -i http://localhost:8080/connectrpc.eliza.v1.ElizaService/Say \
  -H 'Content-Type: application/json' \
  -H 'Connect-Timeout-Ms: 1000' \
  -d '{"sentence":"Hello"}'

Generated unary POST/GET and streaming handlers return deadline_exceeded when their own budget expires. The budget starts when the handler begins; Ktor performs request decoding before it invokes unary handlers. Cancellation by the caller still cancels server work rather than becoming a deadline response.

Unary handlers can return ResponseMessage.Failure or throw ConnectException to send a Connect error. Other handler exceptions reach the application's StatusPages handlers, if installed. The application decides how to respond to those exceptions; without a handler, Ktor's default error handling applies.

Where to go next

  • Code generator for protoc-gen-connect-ktor details
  • Plugins for Connect GET, body limits, compression, and protovalidate
  • Conformance for the support matrix and how CI runs the suite
  • Known limitations before you promise streaming shapes the library does not implement yet
  • Connect protocol when you need the wire-level rules behind those plugins