Instruction to developer: save this file as .cursorrules and place it on the root project directory
Add this skill
npx mdskills install PatrickJS/cursor-kotlin-ktor-developmentComprehensive Kotlin Ktor ruleset with clear architecture, patterns, and testing requirements
1## Instruction to developer: save this file as .cursorrules and place it on the root project directory23## Core Principles4- Follow **SOLID**, **DRY**, **KISS**, and **YAGNI** principles5- Adhere to **OWASP** security best practices6- Break tasks into smallest units and solve problems step-by-step78## Technology Stack9- **Framework**: Kotlin Ktor with Kotlin 2.1.20+10- **JDK**: 21 (LTS)11- **Build**: Gradle with Kotlin DSL12- **Dependencies**: Ktor Server Core/Netty, kotlinx.serialization, Exposed, HikariCP, kotlin-logging, Koin, Kotest1314## Application Structure (Feature-Based)15- **Organize by business features, not technical layers**16- Each feature is self-contained with all related components17- Promotes modularity, reusability, and better team collaboration18- Makes codebase easier to navigate and maintain19- Enables parallel development on different features20```21src/main/kotlin/com/company/app/22├── common/ # Shared utilities, extensions23├── config/ # Application configuration, DI24└── features/25 ├── auth/ # Feature directory26 │ ├── models/27 │ ├── repositories/28 │ ├── services/29 │ └── routes/30 └── users/ # Another feature31 ├── ...32```3334Test structure mirrors the feature-based organization:35```36src/test/kotlin/com/company/app/37├── common/38└── features/39 ├── auth/40 │ ├── models/41 │ ├── repositories/42 │ ├── services/43 │ └── routes/44 └── users/45 ├── ...46```4748## Application Logic Design491. Route handlers: Handle requests/responses only502. Services: Contain business logic, call repositories513. Repositories: Handle database operations524. Entity classes: Data classes for database models535. DTOs: Data transfer between layers5455## Entities & Data Classes56- Use Kotlin data classes with proper validation57- Define Table objects when using Exposed ORM58- Use UUID or auto-incrementing integers for IDs5960## Repository Pattern61```kotlin62interface UserRepository {63 suspend fun findById(id: UUID): UserDTO?64 suspend fun create(user: CreateUserRequest): UserDTO65 suspend fun update(id: UUID, user: UpdateUserRequest): UserDTO?66 suspend fun delete(id: UUID): Boolean67}6869class UserRepositoryImpl : UserRepository {70 override suspend fun findById(id: UUID): UserDTO? = withContext(Dispatchers.IO) {71 transaction {72 Users.select { Users.id eq id }73 .mapNotNull { it.toUserDTO() }74 .singleOrNull()75 }76 }77 // Other implementations...78}79```8081## Service Layer82```kotlin83interface UserService {84 suspend fun getUserById(id: UUID): UserDTO85 suspend fun createUser(request: CreateUserRequest): UserDTO86 suspend fun updateUser(id: UUID, request: UpdateUserRequest): UserDTO87 suspend fun deleteUser(id: UUID)88}8990class UserServiceImpl(91 private val userRepository: UserRepository92) : UserService {93 override suspend fun getUserById(id: UUID): UserDTO {94 return userRepository.findById(id) ?: throw ResourceNotFoundException("User", id.toString())95 }96 // Other implementations...97}98```99100## Route Handlers101```kotlin102fun Application.configureUserRoutes(userService: UserService) {103 routing {104 route("/api/users") {105 get("/{id}") {106 val id = call.parameters["id"]?.let { UUID.fromString(it) }107 ?: throw ValidationException("Invalid ID format")108 val user = userService.getUserById(id)109 call.respond(ApiResponse("SUCCESS", "User retrieved", user))110 }111 // Other routes...112 }113 }114}115```116117## Error Handling118```kotlin119open class ApplicationException(120 message: String,121 val statusCode: HttpStatusCode = HttpStatusCode.InternalServerError122) : RuntimeException(message)123124class ResourceNotFoundException(resource: String, id: String) :125 ApplicationException("$resource with ID $id not found", HttpStatusCode.NotFound)126127fun Application.configureExceptions() {128 install(StatusPages) {129 exception<ResourceNotFoundException> { call, cause ->130 call.respond(cause.statusCode, ApiResponse("ERROR", cause.message ?: "Resource not found"))131 }132 exception<Throwable> { call, cause ->133 call.respond(HttpStatusCode.InternalServerError, ApiResponse("ERROR", "An internal error occurred"))134 }135 }136}137```138139## Testing Strategies and Coverage Requirements140141### Test Coverage Requirements142- **Minimum coverage**: 80% overall code coverage required143- **Critical components**: 90%+ coverage for repositories, services, and validation144- **Test all edge cases**: Empty collections, null values, boundary conditions145- **Test failure paths**: Exception handling, validation errors, timeouts146- **All public APIs**: Must have integration tests147- **Performance-critical paths**: Must have benchmarking tests148149### Unit Testing with Kotest150```kotlin151class UserServiceTest : DescribeSpec({152 describe("UserService") {153 val mockRepository = mockk<UserRepository>()154 val userService = UserServiceImpl(mockRepository)155156 it("should return user when exists") {157 val userId = UUID.randomUUID()158 val user = UserDTO(userId.toString(), "Test User", "test@example.com")159 coEvery { mockRepository.findById(userId) } returns user160161 val result = runBlocking { userService.getUserById(userId) }162163 result shouldBe user164 }165166 it("should throw exception when user not found") {167 val userId = UUID.randomUUID()168 coEvery { mockRepository.findById(userId) } returns null169170 shouldThrow<ResourceNotFoundException> {171 runBlocking { userService.getUserById(userId) }172 }173 }174 }175})176```177178## Route Testing with Ktor 3.x179```kotlin180class UserRoutesTest : FunSpec({181 test("GET /api/users/{id} returns 200 when user exists") {182 val mockService = mockk<UserService>()183 val userId = UUID.randomUUID()184 val user = UserDTO(userId.toString(), "Test User", "test@example.com")185186 coEvery { mockService.getUserById(userId) } returns user187188 testApplication {189 application {190 configureRouting()191 configureDI { single { mockService } }192 }193194 client.get("/api/users/$userId").apply {195 status shouldBe HttpStatusCode.OK196 bodyAsText().let {197 Json.decodeFromString<ApiResponse<UserDTO>>(it)198 }.data shouldBe user199 }200 }201 }202})203```204205## Key Principles for Testable Code2061. **Single Responsibility**: Each method should do one thing well2072. **Pure Functions**: Same input always produces same output2083. **Dependency Injection**: Constructor injection for testable components2094. **Clear Boundaries**: Well-defined inputs and outputs2105. **Small Methods**: Extract complex logic into testable helper functions211212## Configuration Management213```kotlin214// Type-safe configuration215interface AppConfig {216 val database: DatabaseConfig217 val security: SecurityConfig218}219220data class DatabaseConfig(221 val driver: String,222 val url: String,223 val user: String,224 val password: String225)226227// Access in application228fun Application.configureDI() {229 val appConfig = HoconAppConfig(environment.config)230231 install(Koin) {232 modules(module {233 single<AppConfig> { appConfig }234 single { appConfig.database }235 })236 }237}238```239240## Security Best Practices241```kotlin242fun Application.configureSecurity() {243 install(Authentication) {244 jwt("auth-jwt") {245 // JWT configuration246 }247 }248249 install(DefaultHeaders) {250 header(HttpHeaders.XContentTypeOptions, "nosniff")251 header(HttpHeaders.XFrameOptions, "DENY")252 header(HttpHeaders.ContentSecurityPolicy, "default-src 'self'")253 header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")254 }255}256```257258## Health Checks & Monitoring259```kotlin260fun Application.configureMonitoring() {261 val startTime = System.currentTimeMillis()262263 routing {264 get("/health") {265 call.respond(mapOf("status" to "UP", "uptime" to "${(System.currentTimeMillis() - startTime) / 1000}s"))266 }267268 get("/metrics") {269 call.respond(prometheusRegistry.scrape())270 }271 }272273 install(MicrometerMetrics) {274 registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)275 meterBinders = listOf(276 JvmMemoryMetrics(),277 JvmGcMetrics(),278 ProcessorMetrics(),279 JvmThreadMetrics()280 )281 }282}283```284285## Performance Tuning286- **JVM Settings**: `-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:MaxRAMPercentage=75.0`287- **Connection Pooling**: Configure HikariCP with proper sizing based on workload288- **Caching**: Use Caffeine for in-memory caching of frequently accessed data289- **Coroutines**: Use structured concurrency for asynchronous processing290- **Database Queries**: Optimize with proper indexing, batch operations, pagination
Full transparency — inspect the skill content before installing.