spring-boot-kotlin

Spring Boot (Kotlin) Framework Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "spring-boot-kotlin" with this command: npx skills add ar4mirez/samuel/ar4mirez-samuel-spring-boot-kotlin

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

@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&#x3C;UserResponse> {
    return ResponseEntity.ok(userService.getUserById(id))
}

@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
fun getAllUsers(
    @PageableDefault(size = 20) pageable: Pageable,
): ResponseEntity&#x3C;Page&#x3C;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&#x3C;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 @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 -- WebFlux reactive patterns, JPA advanced patterns, testing, security advanced patterns

External References

  • Spring Boot Documentation

  • Spring Kotlin Support

  • Spring Security

  • Spring Data JPA

  • Kotlin + Spring Boot Guide

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

actix-web

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

blazor

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi

No summary provided by upstream source.

Repository SourceNeeds Review