1. Pengenalan Spring Boot
Spring Boot adalah framework Java yang memudahkan pembuatan aplikasi berbasis Spring. Dengan pendekatan convention over configuration dan auto-configuration, Spring Boot memungkinkan developer membuat production-ready REST API dalam hitungan menit.
Spring Boot dibangun di atas Spring Framework dan menyediakan embedded server (Tomcat), auto-configuration, production-ready features (health check, metrics), dan starter dependencies yang mempercepat development.
Mengapa Spring Boot?
| Keunggulan | Penjelasan |
|---|---|
| Auto-Configuration | Spring otomatis mendeteksi dan mengkonfigurasi komponen |
| Embedded Server | Tomcat/Jetty built-in, tidak perlu deploy ke server eksternal |
| Starter Dependencies | Dependency grouping yang sudah dikurasi (spring-boot-starter-web, dll) |
| Production-Ready | Actuator untuk health check, metrics, monitoring |
| Ekosistem Luas | Spring Data, Spring Security, Spring Cloud, dan banyak lagi |
| Komunitas Besar | Dokumentasi lengkap, banyak tutorial, Stack Overflow aktif |
Arsitektur Spring Boot
┌─────────────────────────────────────────────────────────────┐ │ SPRING BOOT ARCHITECTURE │ │ │ │ Client (Browser/App) │ │ │ │ │ ▼ HTTP Request │ │ ┌──────────────────────┐ │ │ │ Controller Layer │ ← @RestController │ │ │ (REST endpoints) │ Terima request, kirim response │ │ └──────────┬───────────┘ │ │ ▼ │ │ ┌──────────────────────┐ │ │ │ Service Layer │ ← @Service │ │ │ (Business Logic) │ Logika bisnis, validasi │ │ └──────────┬───────────┘ │ │ ▼ │ │ ┌──────────────────────┐ │ │ │ Repository Layer │ ← @Repository (JPA) │ │ │ (Data Access) │ Query database │ │ └──────────┬───────────┘ │ │ ▼ │ │ ┌──────────────────────┐ │ │ │ Database │ (MySQL, PostgreSQL, H2) │ │ └──────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
2. Setup Proyek Baru
Cara termudah untuk membuat proyek Spring Boot adalah menggunakan Spring Initializr (start.spring.io) atau IDE seperti IntelliJ IDEA.
Menggunakan Spring Initializr
# 1. Kunjungi https://start.spring.io/ # 2. Pilih: # - Project: Maven # - Language: Java # - Spring Boot: 3.3.x (terbaru) # - Group: com.example # - Artifact: demo-api # - Packaging: Jar # - Java: 21 # - Dependencies: Spring Web, Spring Data JPA, H2 Database, # Spring Security, Lombok # 3. Klik "Generate" → download zip → extract # Struktur folder: demo-api/ ├── src/ │ ├── main/ │ │ ├── java/com/example/demoapi/ │ │ │ ├── DemoApiApplication.java ← Main class │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── repository/ │ │ │ └── model/ │ │ └── resources/ │ │ ├── application.properties ← Konfigurasi │ │ ├── static/ │ │ └── templates/ │ └── test/ └── pom.xml ← Dependencies
pom.xml — Dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="...">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Demo API</name>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Web — REST API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA — Database ORM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database — In-memory untuk development -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Lombok — Mengurangi boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Menjalankan Aplikasi
# Jalankan dari terminal ./mvnw spring-boot:run # Atau build dulu lalu run ./mvnw clean package -DskipTests java -jar target/demo-api-0.0.1-SNAPSHOT.jar # Aplikasi berjalan di http://localhost:8080 # Console output: # Tomcat started on port 8080 (http) # Started DemoApiApplication in 2.3 seconds
application.properties
# Server server.port=8080 # H2 Database (in-memory) spring.datasource.url=jdbc:h2:mem:demo spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.h2.console.enabled=true spring.h2.console.path=/h2-console # JPA / Hibernate spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.database-platform=org.hibernate.dialect.H2Dialect # Application name spring.application.name=demo-api
3. Dependency Injection (DI)
Dependency Injection adalah prinsip inti dari Spring. Alih-alih membuat objek dependency secara manual, Spring akan menyuntikkan (inject) dependency secara otomatis. Ini membuat kode lebih modular, testable, dan loosely coupled.
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// ===== Component — Generic bean =====
@Component
public class EmailService {
public void kirimEmail(String ke, String pesan) {
System.out.println("Email ke " + ke + ": " + pesan);
}
}
// ===== Service — Business logic layer =====
@Service
public class UserService {
// Constructor Injection (RECOMMENDED)
private final EmailService emailService;
private final UserRepository userRepository;
// @Autowired bisa dihilangkan jika hanya 1 constructor
public UserService(EmailService emailService,
UserRepository userRepository) {
this.emailService = emailService;
this.userRepository = userRepository;
}
public void daftarPengguna(String nama, String email) {
User user = new User(nama, email);
userRepository.save(user);
emailService.kirimEmail(email, "Selamat datang, " + nama + "!");
}
}
// Selain constructor injection, ada juga:
// Field Injection (tidak direkomendasikan, sulit di-test)
// @Autowired
// private EmailService emailService;
// Setter Injection
// private EmailService emailService;
// @Autowired
// public void setEmailService(EmailService service) {
// this.emailService = service;
// }
Stereotype Annotations
| Annotation | Layer | Gunanya |
|---|---|---|
@Component | Umum | Generic Spring-managed bean |
@Service | Business | Service layer (logika bisnis) |
@Repository | Data | Data access layer (database) |
@Controller | Web | Web MVC controller |
@RestController | REST API | @Controller + @ResponseBody (JSON) |
Bean Scopes
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
// Singleton (default) — satu instance untuk seluruh aplikasi
@Component
@Scope("singleton")
public class SingletonBean { }
// Prototype — instance baru setiap kali di-inject
@Component
@Scope("prototype")
public class PrototypeBean { }
// Request — instance baru per HTTP request (web only)
// @Component @Scope("request")
// Session — instance baru per user session (web only)
// @Component @Scope("session")
Selalu gunakan constructor injection (bukan field injection). Constructor injection membuat dependency bisa di-test dengan mudah (bisa mock), field immutable (final), dan komponen tidak bisa dibuat tanpa dependency yang lengkap. Field injection terlihat lebih ringkas tapi membuat testing lebih sulit.
4. REST Controller
REST Controller menangani HTTP request dan mengembalikan response (biasanya dalam format JSON). Spring Boot membuat ini sangat mudah dengan anotasi seperti @GetMapping, @PostMapping, dll.
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.*;
// Entity — merepresentasikan tabel di database
@Entity
@Table(name = "produk")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Produk {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Nama tidak boleh kosong")
@Size(min = 2, max = 100)
private String nama;
@NotNull(message = "Harga tidak boleh kosong")
@Positive(message = "Harga harus positif")
private Double harga;
@NotNull
@Min(0)
private Integer stok;
@NotBlank
private String kategori;
private boolean aktif = true;
}
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProdukRepository extends JpaRepository<Produk, Long> {
// Spring Data otomatis buat query dari method name!
List<Produk> findByKategori(String kategori);
List<Produk> findByNamaContainingIgnoreCase(String keyword);
List<Produk> findByHargaBetween(Double min, Double max);
List<Produk> findByAktifTrue();
// Custom JPQL query
@Query("SELECT p FROM Produk p WHERE p.harga = " +
"(SELECT MAX(p2.harga) FROM Produk p2)")
Produk findTermahal();
// Native SQL query
@Query(value = "SELECT * FROM produk WHERE stok > 0",
nativeQuery = true)
List<Produk> findStokTersedia();
}
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProdukService {
private final ProdukRepository repository;
public ProdukService(ProdukRepository repository) {
this.repository = repository;
}
public List<Produk> getAll() {
return repository.findAll();
}
public Produk getById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Produk dengan ID " + id + " tidak ditemukan"));
}
public Produk create(Produk produk) {
return repository.save(produk);
}
public Produk update(Long id, Produk update) {
Produk existing = getById(id);
existing.setNama(update.getNama());
existing.setHarga(update.getHarga());
existing.setStok(update.getStok());
existing.setKategori(update.getKategori());
return repository.save(existing);
}
public void delete(Long id) {
Produk existing = getById(id);
repository.delete(existing);
}
public List<Produk> getByKategori(String kategori) {
return repository.findByKategori(kategori);
}
public List<Produk> search(String keyword) {
return repository.findByNamaContainingIgnoreCase(keyword);
}
}
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/produk")
public class ProdukController {
private final ProdukService service;
public ProdukController(ProdukService service) {
this.service = service;
}
// GET /api/produk — Ambil semua produk
@GetMapping
public ResponseEntity<List<Produk>> getAll() {
return ResponseEntity.ok(service.getAll());
}
// GET /api/produk/{id} — Ambil produk by ID
@GetMapping("/{id}")
public ResponseEntity<Produk> getById(@PathVariable Long id) {
return ResponseEntity.ok(service.getById(id));
}
// POST /api/produk — Buat produk baru
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Produk create(@Valid @RequestBody Produk produk) {
return service.create(produk);
}
// PUT /api/produk/{id} — Update produk
@PutMapping("/{id}")
public ResponseEntity<Produk> update(
@PathVariable Long id,
@Valid @RequestBody Produk produk) {
return ResponseEntity.ok(service.update(id, produk));
}
// DELETE /api/produk/{id} — Hapus produk
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
// GET /api/produk/kategori/{kategori} — Filter by kategori
@GetMapping("/kategori/{kategori}")
public ResponseEntity<List<Produk>> getByKategori(
@PathVariable String kategori) {
return ResponseEntity.ok(service.getByKategori(kategori));
}
// GET /api/produk/search?q=keyword — Cari produk
@GetMapping("/search")
public ResponseEntity<List<Produk>> search(
@RequestParam String q) {
return ResponseEntity.ok(service.search(q));
}
}
HTTP Methods pada REST
| Method | URL | Aksi | Annotation |
|---|---|---|---|
| GET | /api/produk | Ambil semua | @GetMapping |
| GET | /api/produk/1 | Ambil by ID | @GetMapping("/{id}") |
| POST | /api/produk | Buat baru | @PostMapping |
| PUT | /api/produk/1 | Update | @PutMapping("/{id}") |
| DELETE | /api/produk/1 | Hapus | @DeleteMapping("/{id}") |
5. JPA — Database Persistence
Spring Data JPA adalah modul yang menyederhanakan akses database. Dengan mendefinisikan interface JpaRepository, Spring otomatis membuat implementasi CRUD tanpa perlu menulis SQL manual.
One-to-Many Relationship
import jakarta.persistence.*;
import lombok.*;
import java.util.List;
// ===== Parent Entity: Kategori =====
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Kategori {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String nama;
private String deskripsi;
// One Kategori → Many Produk
@OneToMany(mappedBy = "kategori", cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
@ToString.Exclude // Hindari infinite loop
private List<Produk> daftarProduk;
}
// ===== Child Entity: Produk =====
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Produk {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nama;
private Double harga;
private Integer stok;
// Many Produk → One Kategori
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "kategori_id")
@ToString.Exclude
private Kategori kategori;
}
JpaRepository Methods yang Tersedia Otomatis
| Method | Return | Gunanya |
|---|---|---|
findAll() | List<T> | Ambil semua data |
findById(id) | Optional<T> | Ambil by ID |
save(entity) | T | Insert atau Update |
saveAll(list) | List<T> | Batch insert/update |
deleteById(id) | void | Hapus by ID |
delete(entity) | void | Hapus entity |
count() | long | Hitung total record |
existsById(id) | boolean | Cek keberadaan |
6. Global Exception Handling
Spring Boot memungkinkan kita menangani error secara terpusat menggunakan @ControllerAdvice dan @ExceptionHandler. Ini menghindari duplikasi try-catch di setiap controller.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
// Custom Exception
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
// Error Response DTO
public record ApiError(
LocalDateTime timestamp,
int status,
String error,
String message,
Map<String, String> validationErrors
) {}
// Global Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {
// Handle Resource Not Found (404)
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(
ResourceNotFoundException ex) {
ApiError error = new ApiError(
LocalDateTime.now(),
404,
"Not Found",
ex.getMessage(),
null
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
// Handle Validation Errors (400)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(e ->
errors.put(e.getField(), e.getDefaultMessage())
);
ApiError error = new ApiError(
LocalDateTime.now(),
400,
"Validation Failed",
"Data tidak valid",
errors
);
return ResponseEntity.badRequest().body(error);
}
// Handle generic errors (500)
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGeneral(Exception ex) {
ApiError error = new ApiError(
LocalDateTime.now(),
500,
"Internal Server Error",
"Terjadi kesalahan pada server",
null
);
return ResponseEntity.internalServerError().body(error);
}
}
Pisahkan layer dengan jelas: Controller hanya menangani HTTP (request/response), Service berisi logika bisnis, Repository hanya akses database. Jangan letakkan query SQL di controller! Ini prinsip Separation of Concerns yang membuat kode lebih maintainable.
7. Spring Security Basics
Spring Security adalah framework untuk mengamankan aplikasi Spring. Secara default, semua endpoint dilindungi. Kita perlu mengkonfigurasi akses mana yang publik dan mana yang memerlukan autentikasi.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// Konfigurasi Security Filter Chain
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable CSRF untuk REST API
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// Endpoint publik — tidak perlu login
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/h2-console/**").permitAll()
// GET untuk semua user
.requestMatchers(HttpMethod.GET, "/api/produk/**").permitAll()
// POST, PUT, DELETE hanya untuk ADMIN
.requestMatchers(HttpMethod.POST, "/api/produk/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/produk/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/produk/**").hasRole("ADMIN")
// Semua request lain perlu autentikasi
.anyRequest().authenticated()
)
.httpBasic(); // Basic Authentication
return http.build();
}
// In-memory user (untuk development)
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin123"))
.roles("ADMIN")
.build();
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("user123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
// Password encoder — enkripsi password
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Testing API dengan Authentication
# GET semua produk (publik — tanpa auth)
curl http://localhost:8080/api/produk
# POST buat produk baru (perlu auth ADMIN)
curl -X POST http://localhost:8080/api/produk \
-H "Content-Type: application/json" \
-u admin:admin123 \
-d '{"nama":"Laptop","harga":12000000,"stok":10,"kategori":"Elektronik"}'
# GET produk by ID
curl http://localhost:8080/api/produk/1
# PUT update produk
curl -X PUT http://localhost:8080/api/produk/1 \
-H "Content-Type: application/json" \
-u admin:admin123 \
-d '{"nama":"Laptop Gaming","harga":15000000,"stok":5,"kategori":"Elektronik"}'
# DELETE produk (admin only)
curl -X DELETE http://localhost:8080/api/produk/1 -u admin:admin123
# Coba akses tanpa auth (akan 401 Unauthorized)
curl http://localhost:8080/api/produk -X POST \
-H "Content-Type: application/json" \
-d '{"nama":"Test"}'
# Response: 401 Unauthorized
JWT Authentication (Produksi)
// Flow JWT Authentication:
// 1. Client POST /api/auth/login {username, password}
// 2. Server validasi → generate JWT token
// 3. Client simpan token (localStorage / cookie)
// 4. Client kirim token di header: Authorization: Bearer <token>
// 5. Server validasi token di setiap request
// Dependencies untuk JWT (tambahkan di pom.xml):
// io.jsonwebtoken:jjwt-api:0.12.5
// io.jsonwebtoken:jjwt-impl:0.12.5
// io.jsonwebtoken:jjwt-jackson:0.12.5
// Contoh generate token (simplified):
// String token = Jwts.builder()
// .subject(username)
// .claim("roles", roles)
// .issuedAt(new Date())
// .expiration(new Date(System.currentTimeMillis() + 3600000))
// .signWith(secretKey)
// .compact();
// Di produksi, gunakan JWT bukan Basic Auth!
8. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Spring Boot: