spring-boot-kotlin
|
Works with
---
name: spring-boot-kotlin
description: |
license: MIT
---
# Spring Boot (Kotlin) Framework Guide
> Applies to: Spring Boot 3.x, Kotlin 1.9+, REST APIs, Microservices
> Use with: `.claude/skills/kotlin-guide/SKILL.md` for 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 `@Autowired` needed)
- Never use `lateinit var` for dependencies -- constructor injection is idiomatic
- Use `@Transactional(readOnly = true)` for all read-only operations
- Use `@Valid` on 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 `@ConfigurationProperties` data classes for type-safe configuration
- Prefer Kotlin data classes for DTOs with `@field:` annotation target for validation
- Use `open` modifier or `plugin.spring` (auto-opens `@Component` classes)
### Entity Design
- Use `data class` for JPA entities with `plugin.jpa` (auto-generates no-arg constructors)
- Prefer `val` for entity properties, use `copy()` for updates
- Use UUID or auto-generated IDs; nullable `val id: UUID? = null` for new entities
- Use `@CreationTimestamp` and `@UpdateTimestamp` for audit fields
- Keep entities in `model/entity/`, DTOs in `model/dto/`
### Service Layer
- Mark all service classes with `@Service`
- Use `@Transactional` for 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 `@RestController` with `@RequestMapping` for base paths
- Return `ResponseEntity<T>` with explicit status codes
- Use `@Valid @RequestBody` for all incoming request bodies
- Use `@PreAuthorize` for method-level security
- Use `@PageableDefault` for pagination parameters
## Key Patterns
### Application Entry Point
```kotlin
@SpringBootApplication
class MyProjectApplication
fun main(args: Array<String>) {
runApplication<MyProjectApplication>(*args)
}
```
### Type-Safe Configuration
```kotlin
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(
val secret: String,
val expiration: Long,
)
```
```yaml
# application.yml
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-here}
expiration: 86400000
```
### Entity with Data Class
```kotlin
@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
```kotlin
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
```kotlin
@Repository
interface UserRepository : JpaRepository<User, UUID> {
fun findByEmail(email: String): User?
fun existsByEmail(email: String): Boolean
}
```
### Service
```kotlin
@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
```kotlin
@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
```kotlin
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
```kotlin
@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
```kotlin
@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:
```kotlin
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
```
Suspend functions in controllers are automatically handled by Spring WebFlux:
```kotlin
@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](references/patterns.md).
## Commands
```bash
# 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 `@Valid` for 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 var` for 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](references/patterns.md) -- WebFlux reactive patterns, JPA advanced patterns, testing, security advanced patterns
## External References
- [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/)
- [Spring Kotlin Support](https://docs.spring.io/spring-framework/reference/languages/kotlin.html)
- [Spring Security](https://docs.spring.io/spring-security/reference/index.html)
- [Spring Data JPA](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)
- [Kotlin + Spring Boot Guide](https://spring.io/guides/tutorials/spring-boot-kotlin/)More Mobile skills
animation-vocabulary
emilkowalski/skills
Reverse-lookup glossary that turns a vague description of a web animation or motion effect into its exact term ("the bouncy thing when a popover opens" → Pop in; "the iOS rubber-band scroll" → Rubber-banding). Use when the user asks "what's it called when…", or describes a motion effect without knowing its name and wants the right word to prompt an AI or designer with. For naming an effect, not designing or building one.
xcode-project-setup
firebase/agent-skills
Safely modifies Xcode projects (.pbxproj) to add Swift Packages and link files. Use this skill whenever an iOS project needs dependencies installed (e.g. Firebase, Alamofire).
cross-border-ecommerce
nexscope-ai/ecommerce-skills
Cross-border e-commerce expansion advisor. Scores target markets on 8 weighted dimensions (market size, ecommerce penetration, competition, regulatory complexity, logistics infrastructure, payment ecosystem, cultural distance, IP protection), compares 5 fulfillment models with cost and transit data, provides country-by-country tax/duty compliance guides (EU VAT/IOSS, UK VAT, US sales tax, CA GST, AU GST, JP consumption tax), maps local payment preferences by market, and builds a phased expansion roadmap. No API key required.

