REST API Geliştirme
İlk Boot uygulamamızda bir GET endpoint'i yazdık. Şimdi REST API'lerin tüm mekaniğini ele alıyoruz: kaynakları oluşturmak, okumak, güncellemek ve silmek (CRUD); doğru HTTP metotlarını ve durum kodlarını kullanmak; ist…
REST API Geliştirme
İlk Boot uygulamamızda bir GET endpoint'i yazdık. Şimdi REST API'lerin tüm mekaniğini ele
alıyoruz: kaynakları oluşturmak, okumak, güncellemek ve silmek (CRUD); doğru HTTP metotlarını
ve durum kodlarını kullanmak; istek verisini (@RequestBody, @PathVariable, @RequestParam)
karşılamak ve yanıtı ResponseEntity ile tam kontrol etmek. İyi tasarlanmış bir REST API, bir
sözleşmedir: istemci, ne olduğunu yanıtın kodundan ve yapısından anlar.
REST ve HTTP metotları
REST'te bir kaynak (ör. görev, ürün) bir URL ile temsil edilir; üzerinde yapılan işlem ise HTTP metoduyla belirtilir:
| Metot | Anlam | Tipik durum kodu |
|---|---|---|
GET /api/gorevler | Listele | 200 OK |
GET /api/gorevler/{id} | Tek kaynağı oku | 200 OK / 404 Not Found |
POST /api/gorevler | Yeni kaynak oluştur | 201 Created |
PUT /api/gorevler/{id} | Güncelle (tümünü) | 200 OK |
DELETE /api/gorevler/{id} | Sil | 204 No Content |
Spring'de bu metotlar @GetMapping, @PostMapping, @PutMapping, @DeleteMapping ile
karşılanır. İstek verisi üç kaynaktan gelir:
@RequestBody— istek gövdesindeki JSON'u bir nesneye bağlar (POST/PUT için).@PathVariable— URL'deki yol değişkenini (/{id}) parametreye bağlar.@RequestParam— sorgu parametresini (?kategori=...) parametreye bağlar.
Tam CRUD
Örnek 1 (./Ornek1.java) bellek-içi bir depo üzerinde tam bir CRUD API kurar ve açılışta kendi
endpoint'lerini sırayla çağırır: POST ile oluşturur, GET ile okur, PUT ile günceller, DELETE ile
siler. JSON↔nesne dönüşümünü Spring Boot (Jackson) otomatik yapar; sen yalnızca metotları yazarsın:
@PostMapping public Gorev olustur(@RequestBody Gorev g) { ... } @GetMapping("/{id}") public Gorev bul(@PathVariable Long id) { ... } @PutMapping("/{id}") public Gorev guncelle(@PathVariable Long id, @RequestBody Gorev g) { ... } @DeleteMapping("/{id}") public void sil(@PathVariable Long id) { ... }
ResponseEntity ve durum kodları
Bir metottan doğrudan nesne döndürürsen Spring 200 OK varsayar. Ama doğru REST semantiği için
duruma göre farklı kodlar dönmelisin: oluşturmada 201, bulunamadıkta 404, silmede 204.
ResponseEntity yanıtın gövdesini, durum kodunu ve başlıklarını tam kontrol etmeni sağlar:
return ResponseEntity.created(URI.create("/api/urunler/" + id)).body(u); // 201 + Location return (u != null) ? ResponseEntity.ok(u) : ResponseEntity.notFound().build(); // 200 / 404 return ResponseEntity.noContent().build(); // 204
Örnek 2 (./Ornek2.java) bunları canlı gösterir: POST → 201 (+ Location başlığı), var olan
kaynak → 200, olmayan → 404, silme → 204. (Doğru durum kodu döndürmek, istemcinin hata yönetimini
kolaylaştırır.)
En sık kullanılan kodlar: 2xx başarı (200 OK, 201 Created, 204 No Content), 4xx istemci hatası (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found), 5xx sunucu hatası.
Filtreleme ve özel başlıklar
Listeleme endpoint'leri genelde filtreleme/sıralama parametreleri alır. @RequestParam opsiyonel
(required = false) veya varsayılan değerli (defaultValue = "...") olabilir. Yanıta gövdeye ek
olarak özel başlıklar da koyabilirsin (ör. toplam sonuç sayısı):
@GetMapping("/ara") public ResponseEntity<List<Urun>> ara( @RequestParam(required = false) String kategori, @RequestParam(defaultValue = "1000000") double maxFiyat) { return ResponseEntity.ok().header("X-Toplam-Adet", ...).body(sonuc); }
Örnek 3 (./Ornek3.java) kategori/fiyat filtresi, sıralama ve özel X-Toplam-Adet başlığını
gösterir.
İyi REST tasarımı için ipuçları
- Kaynak odaklı URL'ler:
/api/gorevler/{id}(fiil değil, isim). İşlemi metot belirtir. - Doğru durum kodları: Her zaman 200 dönme; 201/204/404/400'ü anlamına göre kullan.
- DTO kullan: Entity'leri doğrudan dışarı açma; istek/yanıt için ayrı DTO'lar daha güvenlidir (bunu katmanlı mimari ve JPA bölümlerinde derinleştireceğiz).
- Tutarlılık: Tarih formatı, hata gövdesi, isimlendirme API genelinde aynı olsun.
Özet
REST'in HTTP metotları ve durum kodlarıyla nasıl çalıştığını; tam CRUD'u (@RequestBody,
@PathVariable); ResponseEntity ile durum/başlık kontrolünü (201/200/404/204) ve @RequestParam
ile filtrelemeyi gerçek, canlı bir API üzerinde gördük. Şimdiye kadar veriyi bellekte tuttuk;
sırada onu kalıcı bir veritabanına yazmanın modern yolu: Spring Data JPA.
▶ Kod Örnekleri(3)
Ornek1
ortam gerekir1// Ornek1: Tam CRUD REST API — GET, POST, PUT, DELETE (bellek-içi depo).
2// @RequestBody ile JSON gövdesi nesneye, @PathVariable ile yol değişkeni parametreye bağlanır.
3// Çalıştırma: portal derleyip gömülü Tomcat ile başlatır, self-test çıktısını alır.
4package com.egitim.springboot.rest;
5
6import org.springframework.boot.CommandLineRunner;
7import org.springframework.boot.SpringApplication;
8import org.springframework.boot.autoconfigure.SpringBootApplication;
9import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
10import org.springframework.context.annotation.Bean;
11import org.springframework.http.MediaType;
12import org.springframework.web.bind.annotation.*;
13import org.springframework.web.client.RestClient;
14
15import java.util.Map;
16import java.util.concurrent.ConcurrentHashMap;
17import java.util.concurrent.atomic.AtomicLong;
18
19@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
20public class Ornek1 {
21
22 public static void main(String[] args) {
23 SpringApplication.run(Ornek1.class, args);
24 }
25
26 // Mutable görev (JSON'dan bağlanabilmesi için get/set'li basit bir sınıf).
27 static class Gorev {
28 public Long id;
29 public String baslik;
30 public boolean tamamlandi;
31 public Gorev() {}
32 public Gorev(String baslik) { this.baslik = baslik; }
33 }
34
35 @RestController
36 @RequestMapping("/api/gorevler")
37 static class GorevController {
38 private final Map<Long, Gorev> depo = new ConcurrentHashMap<>();
39 private final AtomicLong sayac = new AtomicLong();
40
41 @GetMapping // GET /api/gorevler
42 public java.util.Collection<Gorev> hepsi() { return depo.values(); }
43
44 @GetMapping("/{id}") // GET /api/gorevler/{id}
45 public Gorev bul(@PathVariable Long id) { return depo.get(id); }
46
47 @PostMapping // POST /api/gorevler (yeni kayıt)
48 public Gorev olustur(@RequestBody Gorev g) {
49 g.id = sayac.incrementAndGet();
50 depo.put(g.id, g);
51 return g;
52 }
53
54 @PutMapping("/{id}") // PUT /api/gorevler/{id} (güncelle)
55 public Gorev guncelle(@PathVariable Long id, @RequestBody Gorev g) {
56 g.id = id;
57 depo.put(id, g);
58 return g;
59 }
60
61 @DeleteMapping("/{id}") // DELETE /api/gorevler/{id}
62 public void sil(@PathVariable Long id) { depo.remove(id); }
63 }
64
65 @Bean
66 CommandLineRunner selfTest() {
67 return args -> {
68 RestClient c = RestClient.create("http://localhost:8080");
69 System.out.println("\n================ CRUD SELF-TEST ================");
70
71 // CREATE (POST)
72 Gorev olusan = c.post().uri("/api/gorevler").contentType(MediaType.APPLICATION_JSON)
73 .body(new Gorev("Spring Boot öğren")).retrieve().body(Gorev.class);
74 System.out.println("POST -> oluşturuldu id=" + olusan.id + ", baslik=" + olusan.baslik);
75
76 c.post().uri("/api/gorevler").contentType(MediaType.APPLICATION_JSON)
77 .body(new Gorev("REST API yaz")).retrieve().body(Gorev.class);
78
79 // READ (GET all)
80 System.out.println("GET -> " + c.get().uri("/api/gorevler").retrieve().body(String.class));
81
82 // UPDATE (PUT)
83 Gorev guncel = new Gorev("Spring Boot öğren"); guncel.tamamlandi = true;
84 c.put().uri("/api/gorevler/" + olusan.id).contentType(MediaType.APPLICATION_JSON)
85 .body(guncel).retrieve().body(Gorev.class);
86 System.out.println("PUT -> id=" + olusan.id + " güncellendi (tamamlandi=true)");
87 System.out.println("GET/{id}-> " + c.get().uri("/api/gorevler/" + olusan.id).retrieve().body(String.class));
88
89 // DELETE
90 c.delete().uri("/api/gorevler/" + olusan.id).retrieve().toBodilessEntity();
91 System.out.println("DELETE -> id=" + olusan.id + " silindi");
92 System.out.println("GET -> " + c.get().uri("/api/gorevler").retrieve().body(String.class));
93 System.out.println("================================================");
94 };
95 }
96}Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.
Ornek2
ortam gerekir1// Ornek2: ResponseEntity ve HTTP durum kodları — doğru REST semantiği.
2// 201 Created (+ Location), 200 OK, 404 Not Found, 204 No Content.
3// Çalıştırma: portal derleyip gömülü Tomcat ile başlatır.
4package com.egitim.springboot.rest;
5
6import org.springframework.boot.CommandLineRunner;
7import org.springframework.boot.SpringApplication;
8import org.springframework.boot.autoconfigure.SpringBootApplication;
9import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
10import org.springframework.context.annotation.Bean;
11import org.springframework.http.HttpStatus;
12import org.springframework.http.MediaType;
13import org.springframework.http.ResponseEntity;
14import org.springframework.web.bind.annotation.*;
15import org.springframework.web.client.RestClient;
16
17import java.net.URI;
18import java.util.Map;
19import java.util.concurrent.ConcurrentHashMap;
20import java.util.concurrent.atomic.AtomicLong;
21
22@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
23public class Ornek2 {
24
25 public static void main(String[] args) { SpringApplication.run(Ornek2.class, args); }
26
27 static class Urun {
28 public Long id;
29 public String ad;
30 public Urun() {}
31 public Urun(String ad) { this.ad = ad; }
32 }
33
34 @RestController
35 @RequestMapping("/api/urunler")
36 static class UrunController {
37 private final Map<Long, Urun> depo = new ConcurrentHashMap<>();
38 private final AtomicLong sayac = new AtomicLong();
39
40 // 201 CREATED + Location başlığı (yeni kaynağın adresi).
41 @PostMapping
42 public ResponseEntity<Urun> olustur(@RequestBody Urun u) {
43 u.id = sayac.incrementAndGet();
44 depo.put(u.id, u);
45 return ResponseEntity.created(URI.create("/api/urunler/" + u.id)).body(u);
46 }
47
48 // 200 OK (bulundu) veya 404 NOT FOUND (yok).
49 @GetMapping("/{id}")
50 public ResponseEntity<Urun> bul(@PathVariable Long id) {
51 Urun u = depo.get(id);
52 return (u != null) ? ResponseEntity.ok(u) : ResponseEntity.notFound().build();
53 }
54
55 // 204 NO CONTENT (silindi, gövde yok).
56 @DeleteMapping("/{id}")
57 public ResponseEntity<Void> sil(@PathVariable Long id) {
58 depo.remove(id);
59 return ResponseEntity.noContent().build();
60 }
61 }
62
63 @Bean
64 CommandLineRunner selfTest() {
65 return args -> {
66 RestClient c = RestClient.create("http://localhost:8080");
67 System.out.println("\n================ DURUM KODLARI ================");
68
69 // POST -> 201 + Location
70 var created = c.post().uri("/api/urunler").contentType(MediaType.APPLICATION_JSON)
71 .body(new Urun("Klavye")).retrieve().toEntity(Urun.class);
72 System.out.println("POST -> " + created.getStatusCode()
73 + " | Location: " + created.getHeaders().getLocation());
74
75 // GET mevcut -> 200
76 var bulundu = c.get().uri("/api/urunler/1").retrieve().toEntity(Urun.class);
77 System.out.println("GET /1 (var) -> " + bulundu.getStatusCode());
78
79 // GET olmayan -> 404 (RestClient 4xx'te exception fırlatır; durumu yakalıyoruz)
80 HttpStatus durum404 = c.get().uri("/api/urunler/999")
81 .exchange((req, res) -> HttpStatus.valueOf(res.getStatusCode().value()));
82 System.out.println("GET /999 (yok) -> " + durum404);
83
84 // DELETE -> 204
85 var silindi = c.delete().uri("/api/urunler/1").retrieve().toBodilessEntity();
86 System.out.println("DELETE /1 -> " + silindi.getStatusCode());
87 System.out.println("===============================================");
88 System.out.println("Doğru durum kodları, REST API'nin sözleşmesidir: istemci ne olduğunu koddan anlar.");
89 };
90 }
91}Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.
Ornek3
ortam gerekir1// Ornek3: Sorgu parametreleriyle filtreleme + özel yanıt başlıkları (header).
2// @RequestParam (zorunlu/opsiyonel/varsayılan) ve ResponseEntity ile header döndürme.
3// Çalıştırma: portal derleyip gömülü Tomcat ile başlatır.
4package com.egitim.springboot.rest;
5
6import org.springframework.boot.CommandLineRunner;
7import org.springframework.boot.SpringApplication;
8import org.springframework.boot.autoconfigure.SpringBootApplication;
9import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
10import org.springframework.context.annotation.Bean;
11import org.springframework.http.ResponseEntity;
12import org.springframework.web.bind.annotation.GetMapping;
13import org.springframework.web.bind.annotation.RequestMapping;
14import org.springframework.web.bind.annotation.RequestParam;
15import org.springframework.web.bind.annotation.RestController;
16import org.springframework.web.client.RestClient;
17
18import java.util.List;
19
20@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
21public class Ornek3 {
22
23 public static void main(String[] args) { SpringApplication.run(Ornek3.class, args); }
24
25 record Urun(int id, String ad, String kategori, double fiyat) {}
26
27 @RestController
28 @RequestMapping("/api/urunler")
29 static class UrunController {
30 private final List<Urun> urunler = List.of(
31 new Urun(1, "Klavye", "aksesuar", 450),
32 new Urun(2, "Mouse", "aksesuar", 250),
33 new Urun(3, "Monitör", "ekran", 3200),
34 new Urun(4, "Laptop", "bilgisayar", 28000));
35
36 // GET /api/urunler/ara?kategori=aksesuar&maxFiyat=400
37 // kategori: opsiyonel; maxFiyat: varsayılan değerli; sirala: varsayılan "fiyat".
38 @GetMapping("/ara")
39 public ResponseEntity<List<Urun>> ara(
40 @RequestParam(required = false) String kategori,
41 @RequestParam(defaultValue = "1000000") double maxFiyat,
42 @RequestParam(defaultValue = "fiyat") String sirala) {
43
44 List<Urun> sonuc = urunler.stream()
45 .filter(u -> kategori == null || u.kategori().equals(kategori))
46 .filter(u -> u.fiyat() <= maxFiyat)
47 .sorted(sirala.equals("ad")
48 ? java.util.Comparator.comparing(Urun::ad)
49 : java.util.Comparator.comparingDouble(Urun::fiyat))
50 .toList();
51
52 // Yanıta özel bir başlık (header) ekliyoruz: sonuç sayısı.
53 return ResponseEntity.ok()
54 .header("X-Toplam-Adet", String.valueOf(sonuc.size()))
55 .body(sonuc);
56 }
57 }
58
59 @Bean
60 CommandLineRunner selfTest() {
61 return args -> {
62 RestClient c = RestClient.create("http://localhost:8080");
63 System.out.println("\n================ FİLTRELEME + HEADER ================");
64
65 var r1 = c.get().uri("/api/urunler/ara?kategori=aksesuar").retrieve().toEntity(String.class);
66 System.out.println("kategori=aksesuar -> X-Toplam-Adet=" + r1.getHeaders().getFirst("X-Toplam-Adet"));
67 System.out.println(" " + r1.getBody());
68
69 var r2 = c.get().uri("/api/urunler/ara?maxFiyat=500&sirala=ad").retrieve().toEntity(String.class);
70 System.out.println("maxFiyat=500, sirala=ad -> X-Toplam-Adet=" + r2.getHeaders().getFirst("X-Toplam-Adet"));
71 System.out.println(" " + r2.getBody());
72 System.out.println("=====================================================");
73 System.out.println("@RequestParam: required=false (opsiyonel), defaultValue (varsayılan).");
74 System.out.println("ResponseEntity ile gövdeye ek olarak özel başlıklar da döndürülebilir.");
75 };
76 }
77}Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.