kotlin ktor development
via PatrickJS/awesome-cursorrules
Kotlin Ktor development with feature-based architecture, SOLID principles, and comprehensive testing.
What is kotlin ktor development?
Cursor rules for building Kotlin Ktor applications with Kotlin 2.1.20+ and JDK 21. Provides guidance on feature-based project structure, layered architecture (routes, services, repositories), dependency injection with Koin, error handling, security best practices, and testing strategies with 80%+ code coverage requirements using Kotest.
- Organize projects 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 type-safe dependency injection using Koin and manage application configuration
- Apply OWASP security best practices including JWT authentication, security headers, and HTTPS enforcement
- Write unit and integration tests with Kotest achieving 80%+ code coverage with edge case and failure path testing
- Set up health checks, metrics collection with Micrometer/Prometheus, and performance monitoring
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
Kotlin and Spring Boot coding standards, project structure, and idiomatic patterns.
Expert guidance for writing Kubernetes and cloud-native documentation with MkDocs.
Multi-cluster Kubernetes dashboard development rules for React/TypeScript frontend and Go/Fiber backend.
Use real, optimized images with proper alt text and responsive constraints instead of placeholders.
Laravel package development guide with PHP 8.3 features and best practices
Expert guidance for Laravel development with TALL Stack (Livewire, Alpine.js, Tailwind CSS) best practices.
