Ktor Resources¶
Integration with Ktor's type-safe routing plugin.
Overview¶
Ktor Resources provides type-safe routing using Kotlin classes. InspeKtor automatically documents routes defined with Resources.
Setup¶
Add the Ktor Resources plugin to your project:
Install the plugin in your application:
Basic Resources¶
Define your resources as classes with the @Resource annotation:
Kotlin
import io.ktor.resources.*
@Resource("/users")
class Users {
@Resource("{id}")
class Id(val parent: Users = Users(), val id: Long)
@Resource("search")
class Search(val parent: Users = Users(), val query: String)
}
Use them in your routes:
Kotlin
@GenerateOpenApi
fun Application.module() {
install(Resources)
routing {
// GET /users
get<Users> {
responds<List<User>>(HttpStatusCode.OK)
}
// GET /users/{id}
get<Users.Id> { params ->
responds<User>(HttpStatusCode.OK)
val userId = params.id
}
// GET /users/search?query=...
get<Users.Search> { params ->
responds<List<User>>(HttpStatusCode.OK)
val searchQuery = params.query
}
}
}
Generated OpenAPI¶
The above generates:
YAML
paths:
/users:
get:
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
/users/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/users/search:
get:
parameters:
- name: query
in: query
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
Query Parameters¶
Resource properties become query parameters:
Kotlin
@Resource("/products")
class Products(
val category: String? = null,
val minPrice: Double? = null,
val maxPrice: Double? = null,
val sort: String = "name",
val page: Int = 1,
val size: Int = 20
)
Usage:
Kotlin
get<Products> { params ->
responds<Page<Product>>(HttpStatusCode.OK)
val products = productService.find(
category = params.category,
minPrice = params.minPrice,
maxPrice = params.maxPrice,
sort = params.sort,
page = params.page,
size = params.size
)
}
Request: GET /products?category=electronics&minPrice=100&sort=price&page=2
Path Parameters¶
Nested resources with parameters:
Kotlin
@Resource("/organizations")
class Organizations {
@Resource("{orgId}")
class ById(val parent: Organizations = Organizations(), val orgId: String) {
@Resource("teams")
class Teams(val parent: ById) {
@Resource("{teamId}")
class ById(val parent: Teams, val teamId: String) {
@Resource("members")
class Members(val parent: ById)
}
}
}
}
Routes:
Kotlin
// GET /organizations/{orgId}/teams/{teamId}/members
get<Organizations.ById.Teams.ById.Members> { params ->
responds<List<Member>>(HttpStatusCode.OK)
val orgId = params.parent.parent.parent.orgId
val teamId = params.parent.teamId
}
HTTP Methods¶
Resources work with all HTTP methods:
Kotlin
@Resource("/articles")
class Articles {
@Resource("{id}")
class Id(val parent: Articles = Articles(), val id: Long)
}
routing {
// GET /articles
get<Articles> { responds<List<Article>>(HttpStatusCode.OK) }
// POST /articles
post<Articles> {
responds<Article>(HttpStatusCode.Created)
val request = call.receive<CreateArticleRequest>()
}
// GET /articles/{id}
get<Articles.Id> { responds<Article>(HttpStatusCode.OK) }
// PUT /articles/{id}
put<Articles.Id> {
responds<Article>(HttpStatusCode.OK)
val request = call.receive<UpdateArticleRequest>()
}
// DELETE /articles/{id}
delete<Articles.Id> { respondsNothing(HttpStatusCode.NoContent) }
}
Descriptions with Resources¶
Use @KtorDescription with resource routes:
Kotlin
@KtorDescription(summary = "List all users")
get<Users> {
responds<List<User>>(HttpStatusCode.OK)
}
@KtorDescription(
summary = "Get user by ID",
description = "Retrieves a user by their unique identifier"
)
get<Users.Id> { params ->
responds<User>(HttpStatusCode.OK)
responds<ErrorResponse>(HttpStatusCode.NotFound)
}
Tags with Resources¶
Apply tags using the @Tag annotation:
Kotlin
@Tag(["Users"])
fun Route.userResourceRoutes() {
get<Users> { }
post<Users> { }
get<Users.Id> { }
put<Users.Id> { }
delete<Users.Id> { }
}
Optional Parameters¶
Use nullable types or default values for optional parameters:
Kotlin
@Resource("/events")
class Events(
val startDate: String? = null, // Optional query param
val endDate: String? = null, // Optional query param
val limit: Int = 100 // Optional with default
)
Complete Example¶
Kotlin
// Resource definitions
@Resource("/api/v1/users")
class UsersResource {
@Resource("{userId}")
class ById(
val parent: UsersResource = UsersResource(),
val userId: Long
) {
@Resource("posts")
class Posts(val parent: ById) {
@Resource("{postId}")
class ById(val parent: Posts, val postId: Long)
}
}
@Resource("search")
class Search(
val parent: UsersResource = UsersResource(),
val query: String,
val active: Boolean? = null,
val page: Int = 1,
val size: Int = 20
)
}
// Route definitions
@GenerateOpenApi
fun Application.module() {
install(Resources)
routing {
@Tag(["Users"])
userRoutes()
}
}
fun Route.userRoutes() {
@KtorDescription(summary = "List all users")
get<UsersResource> {
responds<List<User>>(HttpStatusCode.OK)
}
@KtorDescription(summary = "Search users")
get<UsersResource.Search> { params ->
responds<Page<User>>(HttpStatusCode.OK)
}
@KtorDescription(summary = "Get user by ID")
get<UsersResource.ById> { params ->
responds<User>(HttpStatusCode.OK)
responds<ErrorResponse>(HttpStatusCode.NotFound)
}
@KtorDescription(summary = "Get user's posts")
get<UsersResource.ById.Posts> { params ->
responds<List<Post>>(HttpStatusCode.OK)
}
@KtorDescription(summary = "Get specific post")
get<UsersResource.ById.Posts.ById> { params ->
responds<Post>(HttpStatusCode.OK)
}
}
Best Practices¶
- Organize resources by domain - Group related resources together
- Use meaningful class names -
Users.ByIdis clearer thanUsers.Id - Provide defaults for optional parameters
- Document query parameters in KtorDescription
- Keep nesting reasonable - Deeply nested resources are hard to use