Type-safe HTTP endpoint contracts for Ktor — bind routing, request/response types, and OpenAPI generation in one place.
ktor-typed-endpoint lets you define each API endpoint as a single typed object that captures:
- The HTTP method (GET / POST / PUT / PATCH / DELETE)
- The Ktor
@Resourcefor type-safe path/query parameters - The request and response body types — deserialized and passed as a typed argument to the handler
- The success HTTP status code
Route registration and OpenAPI documentation are both driven from this one contract, eliminating duplication and keeping the API definition as the single source of truth.
Ktor's @Resource gives you type-safe path and query parameters. But it stops there — the HTTP method, request/response body types, and success status code have no type-level home.
Before — routing, serialization, and documentation are scattered across three separate blocks that must be kept in sync manually:
// 1. Route registration
post<Books> {
val request = call.receive<CreateBookRequest>()
val book = createBook(request)
call.respond(HttpStatusCode.Created, book)
}
// 2. OpenAPI documentation — written separately, easy to forget or get out of sync
describe {
tag("books")
requestBody { schema = jsonSchema<CreateBookRequest>() }
responses {
HttpStatusCode.Created { schema = jsonSchema<BookResponse>() }
}
}After — one contract object is the single source of truth:
// 1. Define the contract once
object PostBook : PostEndpointContract<Books, CreateBookRequest, BookResponse>(
successStatusCode = HttpStatusCode.Created,
)
// 2. Register the route — routing, serialization, status code, and OpenAPI are all handled
endpoint(PostBook) { _, request ->
createBook(request)
}- Kotlin 2.4.10+ — earlier compilers may fail to read the published Kotlin/Native, JS, and Wasm klibs for non-JVM/Android targets.
- Ktor 3.5.x —
ktor-typed-endpoint-ktor-server's public API is built against Ktor 3.5.2; other Ktor 3.x versions should generally work, but matching your app's Ktor version to the same 3.5.x line is recommended. - Android:
compileSdk36,minSdk21+.
// settings.gradle.kts
repositories {
mavenCentral()
}// shared module (contracts live here — KMP: JVM + Android)
implementation("io.github.hayatoyagi:ktor-typed-endpoint-core:<version>")
// server module (route registration + OpenAPI)
implementation("io.github.hayatoyagi:ktor-typed-endpoint-ktor-server:<version>")@Serializable
@Resource("/v1")
class ApiRoutes {
@ApiTag("books")
@Serializable
@Resource("books")
class Books(val parent: ApiRoutes = ApiRoutes()) {
@Serializable
@Resource("{id}")
data class ById(val parent: Books = Books(), val id: String)
}
}object GetBooks : GetEndpointContract<ApiRoutes.Books, BookListResponse>()
object PostBook : PostEndpointContract<ApiRoutes.Books, CreateBookRequest, BookResponse>(
successStatusCode = HttpStatusCode.Created,
)
object PutBook : PutEndpointContract<ApiRoutes.Books.ById, UpdateBookRequest, BookResponse>()
object PatchBook : PatchEndpointContract<ApiRoutes.Books.ById, PatchBookRequest, BookResponse>()fun Route.bookRoutes() {
endpoint(GetBooks) {
bookRepository.findAll()
}
endpoint(PostBook) { _, request ->
bookRepository.create(request)
}
endpoint(PutBook) { resource, request ->
bookRepository.replace(resource.id, request)
}
endpoint(PatchBook) { resource, request ->
bookRepository.update(resource.id, request)
}
}Route registration, request deserialization, response serialization, and OpenAPI documentation are all handled automatically.
Annotate a parent resource to group all its endpoints in Swagger UI. Tags are inherited by child resources automatically.
@ApiTag("books") // All /books/** endpoints get the "books" tag
@Serializable
@Resource("books")
class Books(val parent: ApiRoutes = ApiRoutes()) {
@Serializable
@Resource("{id}") // Also tagged "books" — no annotation needed
data class ById(val parent: Books = Books(), val id: String) {
@Serializable
@Resource("reviews") // Also tagged "books"
class Reviews(val parent: ById)
}
}Annotate request and response models to add descriptions that flow into the generated OpenAPI spec.
@ApiDescription(
"""
Creates a new book.
Validation rules:
- `title` must not be blank.
- `authorId` must reference an existing author.
""",
)
@Serializable
data class CreateBookRequest(
val title: String,
val authorId: String,
)The description appears as requestBody.description for request models and responses.<status>.description for response models.
The sample module contains a minimal runnable Ktor server that demonstrates all library features — nested resources, tag inheritance, multiple HTTP methods, and @ApiDescription.
./gradlew :sample:runThen open http://localhost:8080/swagger to explore the generated API documentation.
Apache-2.0