Eğitim Portalı/Spring Boot/REST API Geliştirme
Spring Boot03-spring-boot/01-rest-api-gelistirme

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:

MetotAnlamTipik durum kodu
GET /api/gorevlerListele200 OK
GET /api/gorevler/{id}Tek kaynağı oku200 OK / 404 Not Found
POST /api/gorevlerYeni kaynak oluştur201 Created
PUT /api/gorevler/{id}Güncelle (tümünü)200 OK
DELETE /api/gorevler/{id}Sil204 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 gerekir
Ornek1.java
1// 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}
canlı çalıştırma için ortam gerekir

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 gerekir
Ornek2.java
1// 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}
canlı çalıştırma için ortam gerekir

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 gerekir
Ornek3.java
1// 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}
canlı çalıştırma için ortam gerekir

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.