kotlin ktor development
via PatrickJS/awesome-cursorrules
Kotlin Ktor development with SOLID principles, feature-based architecture, and comprehensive testing
What is kotlin ktor development?
Cursor rules for building Kotlin Ktor applications with JDK 21, Gradle, and a feature-based project structure. Provides guidance on layered architecture (routes, services, repositories), error handling, testing strategies with 80%+ coverage requirements, security best practices, and performance tuning.
- Organize code by business features rather than technical layers for better modularity and maintainability
- Implement repository, service, and route handler patterns with clear separation of concerns
- Configure dependency injection with Koin and type-safe application configuration
- Write unit tests with Kotest and integration tests with Ktor's testApplication, targeting 80%+ code coverage
- Apply OWASP security best practices including JWT authentication, security headers, and input validation
- Monitor application health and performance with health checks, metrics, and JVM tuning recommendations
Applies to
File patterns this rule matches.
Rule definition (reference)
Source of truth, from the repository.
Instruction to developer: save this file as .cursorrules and place it on the root project directory
Core Principles
- Follow SOLID, DRY, KISS, and YAGNI principles
- Adhere to OWASP security best practices
- Break tasks into smallest units and solve problems step-by-step
Technology Stack
- Framework: Kotlin Ktor with Kotlin 2.1.20+
- JDK: 21 (LTS)
- Build: Gradle with Kotlin DSL
- Dependencies: Ktor Server Core/Netty, kotlinx.serialization, Exposed, HikariCP, kotlin-logging, Koin, Kotest
Application Structure (Feature-Based)
- Organize by business features, not technical layers
- Each feature is self-contained with all related components
- Promotes modularity, reusability, and better team collaboration
- Makes codebase easier to navigate and maintain
- Enables parallel development on different features
src/main/kotlin/com/company/app/
├── common/ # Shared utilities, extensions
├── config/ # Application configuration, DI
└── features/
├── auth/ # Feature directory
│ ├── models/
│ ├── repositories/
│ ├── services/
│ └── routes/
└── users/ # Another feature
├── ...
Test structure mirrors the feature-based organization:
src/test/kotlin/com/company/app/
├── common/
└── features/
├── auth/
│ ├── models/
│ ├── repositories/
│ ├── services/
│ └── routes/
└── users/
├── ...
Application Logic Design
- Route handlers: Handle requests/responses only
- Services: Contain business logic, call repositories
- Repositories: Handle database operations
- Entity classes: Data classes for database models
- DTOs: Data transfer between layers
Entities & Data Classes
- Use Kotlin data classes with proper validation
- Define Table objects when using Exposed ORM
- Use UUID or auto-incrementing integers for IDs
Repository Pattern
interface UserRepository {
suspend fun findById(id: UUID): UserDTO?
suspend fun create(user: CreateUserRequest): UserDTO
suspend fun update(id: UUID, user: UpdateUserRequest): UserDTO?
suspend fun delete(id: UUID): Boolean
}
class UserRepositoryImpl : UserRepository {
override suspend fun findById(id: UUID): UserDTO? = withContext(Dispatchers.IO) {
transaction {
Users.select { Users.id eq id }
.mapNotNull { it.toUserDTO() }
.singleOrNull()
}
}
// Other implementations...
}
Service Layer
interface UserService {
suspend fun getUserById(id: UUID): UserDTO
suspend fun createUser(request: CreateUserRequest): UserDTO
suspend fun updateUser(id: UUID, request: UpdateUserRequest): UserDTO
suspend fun deleteUser(id: UUID)
}
class UserServiceImpl(
private val userRepository: UserRepository
) : UserService {
override suspend fun getUserById(id: UUID): UserDTO {
return userRepository.findById(id) ?: throw ResourceNotFoundException("User", id.toString())
}
// Other implementations...
}
Route Handlers
fun Application.configureUserRoutes(userService: UserService) {
routing {
route("/api/users") {
get("/{id}") {
val id = call.parameters["id"]?.let { UUID.fromString(it) }
?: throw ValidationException("Invalid ID format")
val user = userService.getUserById(id)
call.respond(ApiResponse("SUCCESS", "User retrieved", user))
}
// Other routes...
}
}
}
Error Handling
open class ApplicationException(
message: String,
val statusCode: HttpStatusCode = HttpStatusCode.InternalServerError
) : RuntimeException(message)
class ResourceNotFoundException(resource: String, id: String) :
ApplicationException("$resource with ID $id not found", HttpStatusCode.NotFound)
fun Application.configureExceptions() {
install(StatusPages) {
exception<ResourceNotFoundException> { call, cause ->
call.respond(cause.statusCode, ApiResponse("ERROR", cause.message ?: "Resource not found"))
}
exception<Throwable> { call, cause ->
call.respond(HttpStatusCode.InternalServerError, ApiResponse("ERROR", "An internal error occurred"))
}
}
}
Testing Strategies and Coverage Requirements
Test Coverage Requirements
- Minimum coverage: 80% overall code coverage required
- Critical components: 90%+ coverage for repositories, services, and validation
- Test all edge cases: Empty collections, null values, boundary conditions
- Test failure paths: Exception handling, validation errors, timeouts
- All public APIs: Must have integration tests
- Performance-critical paths: Must have benchmarking tests
Unit Testing with Kotest
class UserServiceTest : DescribeSpec({
describe("UserService") {
val mockRepository = mockk<UserRepository>()
val userService = UserServiceImpl(mockRepository)
it("should return user when exists") {
val userId = UUID.randomUUID()
val user = UserDTO(userId.toString(), "Test User", "test@example.com")
coEvery { mockRepository.findById(userId) } returns user
val result = runBlocking { userService.getUserById(userId) }
result shouldBe user
}
it("should throw exception when user not found") {
val userId = UUID.randomUUID()
coEvery { mockRepository.findById(userId) } returns null
shouldThrow<ResourceNotFoundException> {
runBlocking { userService.getUserById(userId) }
}
}
}
})
Route Testing with Ktor 3.x
class UserRoutesTest : FunSpec({
test("GET /api/users/{id} returns 200 when user exists") {
val mockService = mockk<UserService>()
val userId = UUID.randomUUID()
val user = UserDTO(userId.toString(), "Test User", "test@example.com")
coEvery { mockService.getUserById(userId) } returns user
testApplication {
application {
configureRouting()
configureDI { single { mockService } }
}
client.get("/api/users/$userId").apply {
status shouldBe HttpStatusCode.OK
bodyAsText().let {
Json.decodeFromString<ApiResponse<UserDTO>>(it)
}.data shouldBe user
}
}
}
})
Key Principles for Testable Code
- Single Responsibility: Each method should do one thing well
- Pure Functions: Same input always produces same output
- Dependency Injection: Constructor injection for testable components
- Clear Boundaries: Well-defined inputs and outputs
- Small Methods: Extract complex logic into testable helper functions
Configuration Management
// Type-safe configuration
interface AppConfig {
val database: DatabaseConfig
val security: SecurityConfig
}
data class DatabaseConfig(
val driver: String,
val url: String,
val user: String,
val password: String
)
// Access in application
fun Application.configureDI() {
val appConfig = HoconAppConfig(environment.config)
install(Koin) {
modules(module {
single<AppConfig> { appConfig }
single { appConfig.database }
})
}
}
Security Best Practices
fun Application.configureSecurity() {
install(Authentication) {
jwt("auth-jwt") {
// JWT configuration
}
}
install(DefaultHeaders) {
header(HttpHeaders.XContentTypeOptions, "nosniff")
header(HttpHeaders.XFrameOptions, "DENY")
header(HttpHeaders.ContentSecurityPolicy, "default-src 'self'")
header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
}
Health Checks & Monitoring
fun Application.configureMonitoring() {
val startTime = System.currentTimeMillis()
routing {
get("/health") {
call.respond(mapOf("status" to "UP", "uptime" to "${(System.currentTimeMillis() - startTime) / 1000}s"))
}
get("/metrics") {
call.respond(prometheusRegistry.scrape())
}
}
install(MicrometerMetrics) {
registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
meterBinders = listOf(
JvmMemoryMetrics(),
JvmGcMetrics(),
ProcessorMetrics(),
JvmThreadMetrics()
)
}
}
Performance Tuning
- JVM Settings:
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:MaxRAMPercentage=75.0 - Connection Pooling: Configure HikariCP with proper sizing based on workload
- Caching: Use Caffeine for in-memory caching of frequently accessed data
- Coroutines: Use structured concurrency for asynchronous processing
- Database Queries: Optimize with proper indexing, batch operations, pagination
Related rules
Senior full-stack TypeScript, React, Node.js guidance with clean architecture, testing, and WHY-oriented reasoning.
Quantitative factor research skills for designing, evaluating, and mining alpha factors in equities markets.
Android development with Jetpack Compose, clean architecture, and Material Design 3.
Angular development with Novo Elements UI library using standalone components.
Expert Angular 18 + TypeScript development with Jest, emphasizing clean code and performance.
Manage Kubernetes clusters, add-ons, stacks, and credentials via the Ankra CLI platform.