Episode ini membahas keamanan input dan penanganan error yang konsisten: Jakarta Bean Validation dengan @Valid dan @NotNull, custom validation annotation, global exception handling dengan @ControllerAdvice dan @ExceptionHandler, serta format error response yang seragam untuk seluruh API.

API yang menerima input tanpa validasi adalah pintu masuk berbagai masalah — data rusak, exception tak terduga, hingga serangan injeksi. Episode 7 mengajarkan dua pilar pertahanan: validasi input dengan Jakarta Bean Validation dan penanganan exception yang terpusat.
Kalian akan menghasilkan API yang menolak data buruk dengan pesan jelas, dan mengembalikan format error yang sama untuk semua endpoint. Ini membedakan API yang dibuat asal-asalan dari API yang production-ready.
Validasi disediakan oleh dependency spring-boot-starter-validation yang membungkus Hibernate Validator — implementasi standar Jakarta Bean Validation:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>Dependency ini wajib ditambahkan secara eksplisit — starter spring-boot-starter-web tidak menyertakannya secara default sejak Spring Boot 2.3.
Tandai field model dengan annotation constraint. Contoh pada class request ItemRequest:
public class ItemRequest {
@NotBlank(message = "Nama tidak boleh kosong")
@Size(max = 100, message = "Nama maksimal 100 karakter")
private String name;
@NotNull(message = "Harga wajib diisi")
@Positive(message = "Harga harus bernilai positif")
private BigDecimal price;
}Constraint yang umum dipakai: @NotBlank untuk teks wajib, @NotNull untuk object wajib, @Size untuk batas panjang, @Min dan @Max untuk angka, serta @Email dan @Pattern untuk format khusus. Berikan pesan yang spesifik agar client memahami masalahnya.
Constraint baru bekerja setelah kalian menambahkan @Valid di parameter controller:
@PostMapping
public ResponseEntity<Item> createItem(@Valid @RequestBody ItemRequest request) {
Item saved = itemService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}Saat payload tidak valid, Spring otomatis melempar MethodArgumentNotValidException dan mengembalikan status 400 Bad Request beserta daftar kesalahan. Tanpa @Valid, seluruh constraint diabaikan.
Terkadang constraint bawaan tidak cukup. Untuk aturan khusus, buat annotation sendiri. Contoh: memastikan harga tidak lebih kecil dari harga minimum yang dikonfigurasi:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = HargaMinimumValidator.class)
public @interface HargaMinimum {
String message() default "Harga di bawah minimum";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}Dan implementasi logic-nya:
public class HargaMinimumValidator
implements ConstraintValidator<HargaMinimum, BigDecimal> {
@Override
public boolean isValid(BigDecimal value,
ConstraintValidatorContext context) {
return value == null || value.compareTo(new BigDecimal("1000")) >= 0;
}
}Annotation custom ini bisa dipakai persis seperti constraint bawaan: @HargaMinimum di atas field. Logika isValid harus menangani nilai null dengan baik agar tidak konflik dengan @NotNull.
Daripada menangani error di setiap controller, pusatkan di satu class dengan @ControllerAdvice. Class ini memproses exception dari seluruh controller:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ItemNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(
ItemNotFoundException ex) {
ApiError error = new ApiError(HttpStatus.NOT_FOUND,
ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(
MethodArgumentNotValidException ex) {
List<String> messages = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(err -> err.getField() + ": " + err.getDefaultMessage())
.toList();
ApiError error = new ApiError(HttpStatus.BAD_REQUEST, messages);
return ResponseEntity.badRequest().body(error);
}
}Dengan pola ini, logic konversi exception menjadi respons berada di satu tempat. Controller tetap bersih, dan penanganan error konsisten di seluruh aplikasi.
Agar client mudah mengonsumsi error, gunakan satu bentuk ApiError di seluruh endpoint:
{
"status": 404,
"message": "Item dengan id 99 tidak ditemukan",
"timestamp": "2026-08-10T08:00:00Z"
}Struktur status, message, dan timestamp ini dipakai untuk semua error — dari validasi sampai exception bisnis. Konsistensi format adalah kunci agar error handling client bisa ditulis sekali untuk semua kasus.
Uji perilaku error API dengan request yang tidak valid:
curl -X POST http://localhost:8080/api/items \
-H "Content-Type: application/json" \
-d '{"name":"","price":-5}'Perintah curl -X POST http://localhost:8080/api/items -d '{"name":"","price":-5}' seharusnya mengembalikan 400 Bad Request dengan daftar field yang bermasalah — hasil dari @Valid dan handler validasi yang kita bangun. Respons JSON yang rapi berarti error handling kalian sudah bekerja.
Episode 7 membekali kalian dengan dua pertahanan API yang kuat: validasi input dengan Jakarta Bean Validation, custom validation annotation, dan global exception handling terpusat dengan @ControllerAdvice serta @ExceptionHandler yang menghasilkan format error konsisten.
Inti yang harus dibawa pulang:
spring-boot-starter-validation wajib ditambahkan untuk mengaktifkan Bean Validation.@NotBlank dan @Size dipasang di model, diaktifkan dengan @Valid.@Constraint dan validator kelas tersendiri.@ControllerAdvice memusatkan penanganan exception dari semua controller.status, message, dan timestamp.curl untuk memastikan status code dan pesan sesuai.Di episode 8 selanjutnya kita akan membahas konfigurasi aplikasi dan properties — perbandingan application.properties vs application.yml, externalized configuration dengan environment variables, profile-specific configuration, hingga pengamanan secret dan credentials. API kalian akan siap beradaptasi dengan berbagai lingkungan.