spring-boot-kotlin
SKILL.md
Spring Boot (Kotlin) Framework Guide
Applies to: Spring Boot 3.x, Kotlin 1.9+, REST APIs, Microservices Use with:
.claude/skills/kotlin-guide/SKILL.mdfor base Kotlin patterns
When to Use
- Enterprise Applications: Proven enterprise-grade framework with vast ecosystem
- Microservices: Excellent with Spring Cloud for distributed systems
- Existing Spring Ecosystem: Leverage Spring libraries, security, data, cloud
- Complex Business Logic: Rich feature set for complex domains
- Team Familiarity: Teams with Java/Spring background transitioning to Kotlin
When NOT to Use
- Lightweight Services: Consider Ktor for simpler, minimal-dependency services
- Mobile Backend: Ktor is more lightweight for mobile-only backends
- Minimal Footprint: Spring Boot has a larger memory and startup footprint
Project Structure
myproject/
├── build.gradle.kts
├── settings.gradle.kts
├── src/
│ ├── main/
│ │ ├── kotlin/com/example/myproject/
│ │ │ ├── MyProjectApplication.kt
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.kt
│ │ │ │ ├── WebConfig.kt
│ │ │ │ └── JwtProperties.kt
│ │ │ ├── controller/
│ │ │ │ └── UserController.kt
│ │ │ ├── service/
│ │ │ │ └── UserService.kt
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.kt
│ │ │ ├── model/
│ │ │ │ ├── entity/User.kt
│ │ │ │ └── dto/UserDto.kt
│ │ │ ├── exception/
│ │ │ │ ├── GlobalExceptionHandler.kt
│ │ │ │ └── Exceptions.kt
│ │ │ └── security/
│ │ │ ├── JwtService.kt
│ │ │ ├── JwtAuthenticationFilter.kt
│ │ │ └── CurrentUser.kt
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ └── db/migration/
│ └── test/kotlin/com/example/myproject/
│ ├── controller/
│ ├── service/
│ └── integration/
└── README.md
Guardrails
Spring-Kotlin Specific
- Use constructor injection (Kotlin default, no
@Autowiredneeded) - Never use
lateinit varfor dependencies -- constructor injection is idiomatic - Use
@Transactional(readOnly = true)for all read-only operations - Use
@Validon all request body parameters for automatic validation - Use Spring profiles (
application-{profile}.yml) for environment config - Never hardcode secrets in config files -- use
${ENV_VAR:default}syntax - Use
@ConfigurationPropertiesdata classes for type-safe configuration - Prefer Kotlin data classes for DTOs with
@field:annotation target for validation - Use
openmodifier orplugin.spring(auto-opens@Componentclasses)
Entity Design
- Use
data classfor JPA entities withplugin.jpa(auto-generates no-arg constructors) - Prefer
valfor entity properties, usecopy()for updates - Use UUID or auto-generated IDs; nullable
val id: UUID? = nullfor new entities - Use
@CreationTimestampand@UpdateTimestampfor audit fields - Keep entities in
model/entity/, DTOs inmodel/dto/
Service Layer
- Mark all service classes with
@Service - Use
@Transactionalfor write operations,@Transactional(readOnly = true)for reads - Throw domain-specific exceptions (
NotFoundException,ConflictException) - Return DTOs from services, never expose entities to controllers
- Use Kotlin null safety: repository methods return
T?for nullable finds
Controller Layer
- Use
@RestControllerwith@RequestMappingfor base paths - Return
ResponseEntity<T>with explicit status codes - Use
@Valid @RequestBodyfor all incoming request bodies - Use
@PreAuthorizefor method-level security - Use
@PageableDefaultfor pagination parameters
Key Patterns
Application Entry Point
@SpringBootApplication
class MyProjectApplication
fun main(args: Array<String>) {
runApplication<MyProjectApplication>(*args)
}
Type-Safe Configuration
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(
val secret: String,
val expiration: Long,
)
# application.yml
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-here}
expiration: 86400000
Entity with Data Class
@Entity
@Table(name = "users")
data class User(
@Id
@GeneratedValue(strategy = GenerationType.UUID)
val id: UUID? = null,
@Column(unique = true, nullable = false)
val email: String,
@Column(nullable = false)
val passwordHash: String,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val role: String = "USER",
@CreationTimestamp
@Column(updatable = false)
val createdAt: Instant? = null,
@UpdateTimestamp
val updatedAt: Instant? = null,
)
DTO with Validation
data class CreateUserRequest(
@field:NotBlank(message = "Email is required")
@field:Email(message = "Invalid email format")
val email: String,
@field:NotBlank(message = "Password is required")
@field:Size(min = 8, message = "Password must be at least 8 characters")
val password: String,
@field:NotBlank(message = "Name is required")
@field:Size(min = 1, max = 100)
val name: String,
)
data class UserResponse(
val id: UUID,
val email: String,
val name: String,
val role: String,
val createdAt: Instant,
) {
companion object {
fun from(user: User): UserResponse = UserResponse(
id = user.id!!,
email = user.email,
name = user.name,
role = user.role,
createdAt = user.createdAt!!,
)
}
}
Repository
@Repository
interface UserRepository : JpaRepository<User, UUID> {
fun findByEmail(email: String): User?
fun existsByEmail(email: String): Boolean
}
Service
@Service
class UserService(
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
) {
@Transactional
fun createUser(request: CreateUserRequest): UserResponse {
if (userRepository.existsByEmail(request.email)) {
throw ConflictException("Email already registered")
}
val user = User(
email = request.email,
passwordHash = passwordEncoder.encode(request.password),
name = request.name,
)
return UserResponse.from(userRepository.save(user))
}
@Transactional(readOnly = true)
fun getUserById(id: UUID): UserResponse {
val user = userRepository.findById(id)
.orElseThrow { NotFoundException("User not found: $id") }
return UserResponse.from(user)
}
@Transactional
fun updateUser(id: UUID, request: UpdateUserRequest): UserResponse {
val user = userRepository.findById(id)
.orElseThrow { NotFoundException("User not found: $id") }
val updated = user.copy(
name = request.name ?: user.name,
email = request.email ?: user.email,
)
return UserResponse.from(userRepository.save(updated))
}
}
Controller
@RestController
@RequestMapping("/api")
class UserController(
private val userService: UserService,
) {
@PostMapping("/register")
fun register(
@Valid @RequestBody request: CreateUserRequest,
): ResponseEntity<UserResponse> {
val user = userService.createUser(request)
return ResponseEntity.status(HttpStatus.CREATED).body(user)
}
@GetMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN') or @userSecurity.isOwner(#id, authentication)")
fun getUser(@PathVariable id: UUID): ResponseEntity<UserResponse> {
return ResponseEntity.ok(userService.getUserById(id))
}
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
fun getAllUsers(
@PageableDefault(size = 20) pageable: Pageable,
): ResponseEntity<Page<UserResponse>> {
return ResponseEntity.ok(userService.getAllUsers(pageable))
}
}
Exception Handling
class NotFoundException(message: String) : RuntimeException(message)
class UnauthorizedException(message: String) : RuntimeException(message)
class ConflictException(message: String) : RuntimeException(message)
data class ErrorResponse(
val timestamp: Instant = Instant.now(),
val status: Int,
val error: String,
val message: String,
val details: List<String>? = null,
)
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException::class)
fun handleNotFound(ex: NotFoundException) = ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse(status = 404, error = "NOT_FOUND", message = ex.message ?: "Not found"))
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
val details = ex.bindingResult.fieldErrors.map { "${it.field}: ${it.defaultMessage}" }
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponse(status = 400, error = "VALIDATION_ERROR", message = "Validation failed", details = details))
}
}
Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig(
private val jwtAuthenticationFilter: JwtAuthenticationFilter,
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http
.csrf { it.disable() }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/api/register", "/api/login").permitAll()
.requestMatchers("/health/**", "/actuator/**").permitAll()
.anyRequest().authenticated()
}
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.build()
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder(12)
}
Custom Annotation for Current User
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@AuthenticationPrincipal
annotation class CurrentUser
Coroutines Integration
Spring Boot supports Kotlin coroutines in controllers and services. Add these dependencies:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
Suspend functions in controllers are automatically handled by Spring WebFlux:
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: UUID): ResponseEntity<UserResponse> {
val user = userService.getUserById(id)
return ResponseEntity.ok(user)
}
For detailed coroutine, WebFlux, testing, and advanced security patterns, see references/patterns.md.
Commands
# Development
./gradlew bootRun
# Build
./gradlew build
# Test
./gradlew test
# Test with coverage
./gradlew test jacocoTestReport
# Format (with ktlint)
./gradlew ktlintFormat
# Lint
./gradlew ktlintCheck
# Build JAR
./gradlew bootJar
# Run JAR
java -jar build/libs/myproject-0.0.1-SNAPSHOT.jar
# Docker build
docker build -t myproject .
Best Practices
DO
- Use data classes for DTOs and entities
- Use Kotlin null safety features (
?.,?:,requireNotNull) - Use
@Transactional(readOnly = true)for read operations - Use constructor injection (default in Kotlin)
- Use
@Validfor request validation - Use Spring profiles for environment configuration
- Use
@field:annotation target for validation annotations on data class properties - Use coroutines for async operations where beneficial
DON'T
- Use
lateinit varfor dependencies (use constructor injection) - Ignore null safety in Spring Data repositories
- Use
!!operator without proper null checks - Mix reactive and blocking code in the same call chain
- Store secrets in configuration files
- Expose JPA entities directly in controller responses
References
For detailed patterns and examples, see:
- references/patterns.md -- WebFlux reactive patterns, JPA advanced patterns, testing, security advanced patterns
External References
Weekly Installs
5
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
13 days ago
Security Audits
Installed on
gemini-cli5
opencode5
codebuddy5
github-copilot5
codex5
kimi-cli5