290 lines
9.0 KiB
Plaintext
290 lines
9.0 KiB
Plaintext
## 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
|
|
1. Route handlers: Handle requests/responses only
|
|
2. Services: Contain business logic, call repositories
|
|
3. Repositories: Handle database operations
|
|
4. Entity classes: Data classes for database models
|
|
5. 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
|
|
```kotlin
|
|
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
|
|
```kotlin
|
|
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
|
|
```kotlin
|
|
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
|
|
```kotlin
|
|
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
|
|
```kotlin
|
|
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
|
|
```kotlin
|
|
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
|
|
1. **Single Responsibility**: Each method should do one thing well
|
|
2. **Pure Functions**: Same input always produces same output
|
|
3. **Dependency Injection**: Constructor injection for testable components
|
|
4. **Clear Boundaries**: Well-defined inputs and outputs
|
|
5. **Small Methods**: Extract complex logic into testable helper functions
|
|
|
|
## Configuration Management
|
|
```kotlin
|
|
// 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
|
|
```kotlin
|
|
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
|
|
```kotlin
|
|
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 |