internal/skills/content/ktor/SKILL.md
Ktor framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Ktor projects, or when the user mentions Ktor. Provides coroutine-based routing, plugins, serialization, and server/client guidelines.
npx skillsauth add ar4mirez/samuel ktorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Applies to: Ktor 2.x, Kotlin 1.9+, Microservices, REST APIs, WebSockets
Ktor is a lightweight, asynchronous framework built by JetBrains for Kotlin. It leverages coroutines for non-blocking I/O and provides a flexible plugin system for extensibility.
Best For: Microservices, lightweight APIs, real-time applications, Kotlin-native projects
Key Characteristics:
testApplication)application.conf with environment variable overridesmyapp/
├── src/main/kotlin/com/example/
│ ├── Application.kt # Entry point, module composition
│ ├── plugins/ # Plugin configuration (one file per plugin)
│ │ ├── Routing.kt
│ │ ├── Serialization.kt
│ │ ├── Security.kt
│ │ ├── StatusPages.kt
│ │ ├── Validation.kt
│ │ └── Databases.kt
│ ├── routes/ # Route definitions (Route extensions)
│ │ ├── UserRoutes.kt
│ │ └── AuthRoutes.kt
│ ├── models/ # Data models, DTOs, table definitions
│ │ ├── User.kt
│ │ └── Requests.kt
│ ├── services/ # Business logic layer
│ │ └── UserService.kt
│ ├── repositories/ # Data access layer (Exposed)
│ │ └── UserRepository.kt
│ └── utils/ # Utilities (JWT, hashing, etc.)
│ └── JwtUtils.kt
├── src/main/resources/
│ ├── application.conf # HOCON configuration
│ └── logback.xml
├── src/test/kotlin/com/example/
│ ├── ApplicationTest.kt # Integration tests with testApplication
│ └── services/UserServiceTest.kt # Unit tests with MockK
├── build.gradle.kts
└── gradle.properties
Conventions:
plugins/Route extension functions in routes/models/ contains Exposed table objects, domain models, and serializable DTOsrepositories/ wraps all database access behind suspend functionsApplication.module() that composes plugins in orderembeddedServer() for simple apps, EngineMain for HOCON-driven startupApplication.ktfun Application.module() {
configureSerialization() // ContentNegotiation first
configureValidation() // RequestValidation before routing
configureSecurity() // Authentication before protected routes
configureStatusPages() // Error handling catches all
configureDatabases() // Database connection pool
configureRouting() // Routes last (depends on all above)
}
application.conf with environment variable overrides via ${?ENV_VAR}database, jwt, server)environment.config.property("path").getString()ktor {
deployment { port = 8080, port = ${?PORT} }
application { modules = [ com.example.ApplicationKt.module ] }
}
database {
url = "jdbc:postgresql://localhost:5432/myapp"
url = ${?DATABASE_URL}
driver = "org.postgresql.Driver"
user = ${?DATABASE_USER}
password = ${?DATABASE_PASSWORD}
maxPoolSize = 10
}
jwt {
secret = ${?JWT_SECRET}
issuer = "myapp"
audience = "myapp-users"
realm = "myapp"
expirationMs = 3600000
}
kotlinx.serialization with ContentNegotiation plugin@Serializable data classesfun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = false // disable in production
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
})
}
}
Route extension functions (not inline in configureRouting)/api/v1/...)authenticate("scheme") blocks to protect routesCreated, NoContent, BadRequest)fun Route.userRoutes(userService: UserService) {
route("/users") {
post {
val request = call.receive<CreateUserRequest>()
val user = userService.createUser(request)
call.respond(HttpStatusCode.Created, user)
}
authenticate("auth-jwt") {
get {
val limit = call.parameters["limit"]?.toIntOrNull() ?: 20
val offset = call.parameters["offset"]?.toLongOrNull() ?: 0
call.respond(userService.getAllUsers(limit, offset))
}
get("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid ID")
call.respond(userService.getUserById(id))
}
}
}
}
RequestValidation plugin for declarative input validationValidationResult.InvalidRequestValidationException in StatusPagesfun Application.configureValidation() {
install(RequestValidation) {
validate<CreateUserRequest> { req ->
val errors = buildList {
if (!req.email.contains("@")) add("Invalid email format")
if (req.password.length < 8) add("Password must be at least 8 characters")
if (req.name.isBlank()) add("Name is required")
}
if (errors.isNotEmpty()) ValidationResult.Invalid(errors)
else ValidationResult.Valid
}
}
}
Authentication plugin with named JWT schemesvalidate block; return null to rejectchallenge block (not raw strings)JWTPrincipal in routes, never parse tokens manuallyfun Application.configureSecurity() {
val config = environment.config
install(Authentication) {
jwt("auth-jwt") {
realm = config.property("jwt.realm").getString()
verifier(JWT.require(Algorithm.HMAC256(config.property("jwt.secret").getString()))
.withAudience(config.property("jwt.audience").getString())
.withIssuer(config.property("jwt.issuer").getString())
.build())
validate { credential ->
val userId = credential.payload.getClaim("userId").asString()
if (userId != null) JWTPrincipal(credential.payload) else null
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized,
ErrorResponse(401, "UNAUTHORIZED", "Token is not valid or has expired"))
}
}
}
}
StatusPages plugin to centralize exception-to-response mappingThrowable handler that logs and returns 500@Serializable
data class ErrorResponse(val status: Int, val error: String, val message: String)
class NotFoundException(message: String) : RuntimeException(message)
class ConflictException(message: String) : RuntimeException(message)
fun Application.configureStatusPages() {
install(StatusPages) {
exception<NotFoundException> { call, cause ->
call.respond(HttpStatusCode.NotFound,
ErrorResponse(404, "NOT_FOUND", cause.message ?: "Resource not found"))
}
exception<RequestValidationException> { call, cause ->
call.respond(HttpStatusCode.BadRequest,
ErrorResponse(400, "VALIDATION_ERROR", cause.reasons.joinToString("; ")))
}
exception<Throwable> { call, cause ->
call.application.environment.log.error("Unhandled exception", cause)
call.respond(HttpStatusCode.InternalServerError,
ErrorResponse(500, "INTERNAL_ERROR", "An unexpected error occurred"))
}
}
}
newSuspendedTransaction for coroutine safetyobject : LongIdTable("name") in models/suspend dbQuery helperResultRow mappingSchemaUtils.create() for dev; prefer migrations for production// Repository pattern with suspend functions
class UserRepository {
private suspend fun <T> dbQuery(block: suspend () -> T): T =
newSuspendedTransaction { block() }
suspend fun findById(id: Long): User? = dbQuery {
Users.select { Users.id eq id }.map(User::fromRow).singleOrNull()
}
suspend fun create(email: String, passwordHash: String, name: String): User = dbQuery {
val id = Users.insertAndGetId {
it[Users.email] = email
it[Users.passwordHash] = passwordHash
it[Users.name] = name
}
Users.select { Users.id eq id }.map(User::fromRow).single()
}
}
suspend functionsNotFoundException, ConflictException), not generic oneswithContext(Dispatchers.IO)withTimeout for external service callsCancellationException correctly (rethrow, never swallow)supervisorScope when child failures should not cancel siblingsclass ApplicationTest {
@Test
fun `create user returns 201`() = testApplication {
application { module() }
val client = createClient {
install(ContentNegotiation) { json() }
}
val response = client.post("/api/users") {
contentType(ContentType.Application.Json)
setBody(CreateUserRequest("[email protected]", "password123", "Test"))
}
assertEquals(HttpStatusCode.Created, response.status)
}
}
class UserServiceTest {
private val repo = mockk<UserRepository>()
private val jwt = mockk<JwtUtils>()
private val service = UserService(repo, jwt)
@Test
fun `createUser with existing email throws ConflictException`() = runBlocking {
coEvery { repo.existsByEmail(any()) } returns true
assertFailsWith<ConflictException> {
service.createUser(CreateUserRequest("[email protected]", "pass1234", "Dup"))
}
}
@AfterTest fun teardown() { clearAllMocks() }
}
testApplication for HTTP-level integration testscoEvery/coVerify for coroutine-aware mockingcreate user with duplicate email throws ConflictException# Development
./gradlew run # Start dev server
./gradlew run -t # Auto-reload on changes
# Build
./gradlew build # Compile + test + check
./gradlew buildFatJar # Build executable fat JAR
java -jar build/libs/app.jar # Run production JAR
# Test
./gradlew test # Run all tests
./gradlew test --tests "*.UserServiceTest" # Specific test class
./gradlew test --tests "*create user*" # Tests matching pattern
# Quality
./gradlew ktlintCheck # Check formatting
./gradlew ktlintFormat # Auto-fix formatting
./gradlew detekt # Static analysis
# Dependencies
./gradlew dependencies # Full dependency tree
./gradlew clean # Clean build artifacts
plugins/suspend functions for all database and external callskotlinx.serialization for JSON (not Jackson/Gson)newSuspendedTransaction for coroutine-safe DB accessapplication.conf with env var overridesGlobalScope for request-scoped workChoose Ktor when: building lightweight microservices, Kotlin is your primary language, you need fast startup times, you want fine-grained control over dependencies, building real-time applications (WebSockets), or creating serverless functions.
Consider alternatives when: you need extensive enterprise integrations (Spring Boot), team is more familiar with Spring, project requires complex security configurations, or you need a large third-party library ecosystem.
For detailed patterns, examples, and advanced topics, see:
development
Zig language guardrails, patterns, and best practices for AI-assisted development. Use when working with Zig files (.zig), build.zig, or when the user mentions Zig. Provides comptime patterns, allocator conventions, C interop guidelines, and testing standards specific to this project's coding standards.
tools
WordPress framework guardrails, patterns, and best practices for AI-assisted development. Use when working with WordPress projects, or when the user mentions WordPress. Provides theme development, plugin architecture, REST API, blocks, and security guidelines.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. Use when testing web apps, automating browser interactions, or debugging frontend issues.
tools
Suite of tools for creating elaborate, multi-component web applications using modern frontend technologies (React, Tailwind CSS, shadcn/ui). Use for complex projects requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX pages.