
Define API endpoints as single typed objects capturing HTTP method, type-safe path/query resources, request/response bodies and success status; auto-register routes and generate OpenAPI with tag/description inheritance.
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:
@Resource for type-safe path/query parametersRoute 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)
}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.compileSdk 36, minSdk 21+.// 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
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:
@Resource for type-safe path/query parametersRoute 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)
}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.compileSdk 36, minSdk 21+.// 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