Error Handling Best Practices in Spring Boot with Kotlin
Master Spring Boot error handling in Kotlin with comprehensive examples: learn to implement global exception handlers, custom error responses, and production-ready error handling strategies
Request validation is a crucial aspect of building robust and secure APIs. In this tutorial, you'll learn how to implement comprehensive request validation in a Spring Boot application using Kotlin. You'll explore different validation techniques, from basic field validation to complex custom validation rules.
To follow this tutorial, you will need:
First, create a new Spring Boot project using Spring Initializr. You'll need the following dependencies:
Add these dependencies to your build.gradle.kts
:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
}
Let's create a simple user registration model to demonstrate validation. Create a new file called UserRegistrationRequest.kt
:
data class UserRegistrationRequest(
@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 long")
val password: String,
@field:NotBlank(message = "Name is required")
@field:Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
val name: String,
@field:Min(value = 18, message = "Age must be at least 18")
val age: Int
)
Create a controller to handle user registration:
@RestController
@RequestMapping("/api/users")
class UserController {
@PostMapping("/register")
fun registerUser(@Valid @RequestBody request: UserRegistrationRequest): ResponseEntity<Any> {
// Registration logic here
return ResponseEntity.ok().build()
}
}
To handle validation errors gracefully, create a global exception handler:
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationErrors(ex: MethodArgumentNotValidException): ResponseEntity<Map<String, List<String>>> {
val errors = ex.bindingResult.fieldErrors
.map { it.defaultMessage ?: "Validation error" }
.sorted()
return ResponseEntity.badRequest()
.body(mapOf("errors" to errors))
}
}
Sometimes you need custom validation logic. Let's create a custom validator to check if a password is strong enough:
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [StrongPasswordValidator::class])
annotation class StrongPassword(
val message: String = "Password must contain at least one uppercase letter, one lowercase letter, and one number",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
class StrongPasswordValidator : ConstraintValidator<StrongPassword, String> {
override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
if (value == null) return false
val hasUppercase = value.any { it.isUpperCase() }
val hasLowercase = value.any { it.isLowerCase() }
val hasNumber = value.any { it.isDigit() }
return hasUppercase && hasLowercase && hasNumber
}
}
Update the UserRegistrationRequest
to use the custom validator:
data class UserRegistrationRequest(
// ... other fields ...
@field:NotBlank(message = "Password is required")
@field:Size(min = 8, message = "Password must be at least 8 characters long")
@field:StrongPassword
val password: String,
// ... other fields ...
)
Sometimes you need to validate fields in relation to each other. Let's create a custom validator for confirming passwords:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [PasswordMatchValidator::class])
annotation class PasswordMatch(
val message: String = "Passwords do not match",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
class PasswordMatchValidator : ConstraintValidator<PasswordMatch, UserRegistrationRequest> {
override fun isValid(value: UserRegistrationRequest, context: ConstraintValidatorContext?): Boolean {
return value.password == value.confirmPassword
}
}
@PasswordMatch
data class UserRegistrationRequest(
// ... other fields ...
val confirmPassword: String
)
Here's how to test your validation using @SpringBootTest
:
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Autowired
private lateinit var objectMapper: ObjectMapper
@Test
fun `should return 400 when email is invalid`() {
val request = UserRegistrationRequest(
email = "invalid-email",
password = "Password123",
confirmPassword = "Password123",
name = "John Doe",
age = 25
)
mockMvc.perform(
MockMvcRequestBuilders.post("/api/users/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(MockMvcResultMatchers.status().isBadRequest)
.andExpect(MockMvcResultMatchers.jsonPath("$.errors").isArray)
.andExpect(MockMvcResultMatchers.jsonPath("$.errors[0]").value("Invalid email format"))
}
}
interface RegistrationValidation
interface UpdateValidation
data class UserRequest(
@field:NotBlank(groups = [RegistrationValidation::class])
val password: String
)
@field:Size(
min = 8,
message = "Password must be at least {min} characters long"
)
@Service
class UserValidationService(
private val userRepository: UserRepository
) {
fun validateEmailUniqueness(email: String) {
if (userRepository.existsByEmail(email)) {
throw EmailAlreadyExistsException("Email already registered")
}
}
}
You've learned how to implement comprehensive request validation in a Spring Boot application using Kotlin. From basic field validation to custom validators and cross-field validation, you now have the tools to ensure your API accepts only valid data.
Q: Why aren't my validation annotations working?
A: Make sure you've added the @Valid
annotation to your controller method parameter and included the spring-boot-starter-validation
dependency.
Q: How can I customize validation messages for different locales?
A: Use message source files in resources/messages_[locale].properties
and reference them in your validation annotations.
Q: What's the difference between @field:
and @get:
prefix in Kotlin validation annotations?
A: @field:
applies the validation to the backing field, while @get:
applies it to the getter. For request validation, you typically want @field:
.
Master Spring Boot error handling in Kotlin with comprehensive examples: learn to implement global exception handlers, custom error responses, and production-ready error handling strategies
Master Role-Based Access Control (RBAC) in Spring Boot applications using Kotlin with practical examples, from basic setup to advanced configurations with method-level security
Learn how to implement secure API key authentication in Spring Boot with Kotlin, including rate limiting, monitoring, and production-ready best practices - complete with practical examples and ready-to-use code