İçeriğe geç
Educora
İleri25 dk17 / 18

Spring Boot ile REST API

Bir Spring Boot projesi oluştur, denetleyici, servis ve depo katmanlarını bağımlılık enjeksiyonuyla bağla, bir CRUD API kur ve onu curl ile test et.

Kendini test et
Bu derste öğreneceklerin
  • Bir Spring Boot projesi oluşturmak ve çalıştırmak
  • @RestController, @Service, @Repository katmanlarını ve bağımlılık enjeksiyonunu açıklamak
  • Doğru HTTP durum kodlarıyla bir CRUD API yazmak ve onu curl ile test etmek

Bir mobil uygulama, örneğin elektronik bir okul karnesi, notları bir sunucudan alır: uygulama bir HTTP isteği gönderir (GET /api/students), sunucu da JSON döndürür. Böyle bir sunucuya REST API denir. Java dünyasında bunu yazmanın en popüler yolu Spring Boot'tur: birkaç ek açıklamayla sıradan sınıflar bir web sunucusuna dönüşür; JSON, veritabanları ve yapılandırma otomatik olarak bağlanır.

Projeyi oluşturmak

  1. 1
    start.spring.io sitesini aç

    Bu, resmî proje oluşturucudur (IntelliJ IDEA Ultimate'te de aynı sihirbaz vardır). Project için Maven ya da Gradle'ı, Language için Java'yı, Java için 21'i seç.

  2. 2
    Üst verileri doldur

    Group com.educora, Artifact school, Name SchoolApi. Paket com.educora.school, ana sınıf ise SchoolApiApplication olur.

  3. 3
    Bağımlılıkları ekle

    ADD DEPENDENCIES düğmesine bas ve Spring Web'i seç. Gerçek projelerde genellikle Spring Data JPA, bir veritabanı sürücüsü ve Validation da eklenir.

  4. 4
    Oluştur ve aç

    GENERATE bir ZIP arşivi indirir. Onu aç, klasörü IDE'de aç ve ana sınıfı çalıştır.

Java
package com.educora.school;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SchoolApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(SchoolApiApplication.class, args);
    }
}
Giriş noktası: @SpringBootApplication ve tek satırlık main

@SpringBootApplication üç şeyi birleştirir: sınıfı bir yapılandırma kaynağı yapar, paketteki bileşen aramasını (component scan) ve otomatik yapılandırmayı açar. Sonuncusu classpath'e bakar: Spring Web oradaysa gömülü Tomcat sunucusu 8080 portunda başlar ve JSON için Jackson bağlanır. Ayrı bir sunucu kurmana gerek yok; uygulama, sıradan bir main metodu olan bir JAR'dır.

Terminal
./mvnw spring-boot:run
Beklenen çıktı
... Tomcat started on port 8080 (http) with context path '/'
... Started SchoolApiApplication in 1.9 seconds (process running for 2.2)
Örnek çıktı (kısaltılmış); Gradle'da ./gradlew bootRun

Katmanlar ve bağımlılık enjeksiyonu

KatmanEk açıklamaGörevi
Denetleyici@RestControllerHTTP isteklerini alır, JSON döndürür
Servis@Serviceiş kuralları: doğrulama, hesaplamalar
Depo@Repositoryveriyi saklar ve okur

Bu ek açıklamalarla işaretlenen sınıfları Spring kendisi oluşturur; bunlara bean denir ve IoC kapsayıcısında tutulur (genellikle her sınıftan tek bir örnek). Bir sınıfın yapıcısı başka bir bean'e ihtiyaç duyarsa Spring onu otomatik olarak verir: buna bağımlılık enjeksiyonu (dependency injection) denir. Sonuçta sınıflar birbirini new ile oluşturmaz ve testte bir bağımlılığı mock ile değiştirmek kolaylaşır; tıpkı JUnit dersindeki gibi.

Sınıf bağımlılıklarını kendisi oluşturur
@RestController
public class StudentController {
    // the controller builds its own dependencies:
    // hard to test, and every class gets its own copy
    private final StudentService service =
            new StudentService(new StudentRepository(), 100);
}
Spring bağımlılığı yapıcıya verir
@RestController
public class StudentController {
    private final StudentService service;

    // Spring creates StudentService once and passes it in
    public StudentController(StudentService service) {
        this.service = service;
    }
}

Öğrenciler için CRUD API

Şimdi öğrenciler için tam bir CRUD API kuralım: oluşturma (Create), okuma (Read), güncelleme (Update), silme (Delete). Basitlik için depo veriyi bellekte, bir ConcurrentHashMap içinde tutar: istekler paralel iş parçacıklarında geldiği için iş parçacığı güvenli bir koleksiyon seçildi.

Java
public record Student(Long id, String name, int score) {}

@Repository
public class StudentRepository {
    private final Map<Long, Student> students = new ConcurrentHashMap<>();
    private final AtomicLong nextId = new AtomicLong(1);

    public List<Student> findAll() {
        return students.values().stream().sorted(Comparator.comparing(Student::id)).toList();
    }

    public Optional<Student> findById(long id) {
        return Optional.ofNullable(students.get(id));
    }

    public Student save(Student s) {
        long id = s.id() != null ? s.id() : nextId.getAndIncrement();
        Student saved = new Student(id, s.name(), s.score());
        students.put(id, saved);
        return saved;
    }

    public boolean deleteById(long id) {
        return students.remove(id) != null;
    }
}
Model ve depo (kısalık için içe aktarmalar gösterilmedi)
Java
@Service
public class StudentService {
    private final StudentRepository repository;
    private final int maxScore;

    public StudentService(StudentRepository repository,
                          @Value("${school.max-score:100}") int maxScore) {
        this.repository = repository;
        this.maxScore = maxScore;
    }

    public List<Student> all() { return repository.findAll(); }

    public Student get(long id) {
        return repository.findById(id).orElseThrow(() ->
                new ResponseStatusException(HttpStatus.NOT_FOUND, "Student " + id + " not found"));
    }

    public Student save(Long id, Student s) {
        if (s.score() < 0 || s.score() > maxScore)
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Score must be 0-" + maxScore);
        if (id != null) get(id);                  // 404 if we update a missing student
        return repository.save(new Student(id, s.name(), s.score()));
    }

    public void delete(long id) {
        if (!repository.deleteById(id)) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
    }
}
Servis: doğrulama kuralları ve application.properties'ten gelen school.max-score
Java
@RestController
@RequestMapping("/api/students")
public class StudentController {
    private final StudentService service;

    public StudentController(StudentService service) {
        this.service = service;
    }

    @GetMapping
    public List<Student> all() { return service.all(); }

    @GetMapping("/{id}")
    public Student one(@PathVariable long id) { return service.get(id); }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Student create(@RequestBody Student student) { return service.save(null, student); }

    @PutMapping("/{id}")
    public Student update(@PathVariable long id, @RequestBody Student student) {
        return service.save(id, student);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable long id) { service.delete(id); }
}
Denetleyici: her metot tek bir HTTP işlemine bağlanır
İstekNe yaparBaşarılı yanıt
GET /api/studentstüm öğrenciler200 OK
GET /api/students/{id}tek öğrenci; yoksa 404200 OK
POST /api/studentsöğrenci oluşturur201 Created
PUT /api/students/{id}öğrenciyi günceller200 OK
DELETE /api/students/{id}öğrenciyi siler204 No Content

Tabloda REST'in temel kuralları görülüyor: URL bir kaynağı çoğul bir isimle adlandırır (/api/students), ne yapılacağını ise HTTP metodu söyler. Bu yüzden /api/getStudents ya da /api/deleteStudent?id=2 gibi yollar kötü üslup sayılır. GET asla veriyi değiştirmemeli, PUT kaynağı bütünüyle değiştirir, sonucu ise durum kodu bildirir; istemci yanıt metnini okumadan ne olduğunu anlar.

@RequestMapping, tüm denetleyici için ortak yolu belirler. @PathVariable URL'deki {id} değerini bir parametreye, @RequestBody ise isteğin JSON gövdesini bir Student nesnesine çevirir; yanıt nesnesini JSON'a Jackson kütüphanesi dönüştürür. ResponseStatusException, her yerden doğru HTTP durumunu (örneğin 404 ya da 400) döndürmeyi sağlar. @Value("${school.max-score:100}") ayarı yapılandırmadan okur; iki noktadan sonraki 100 varsayılan değerdir.

Text
spring.application.name=school-api
server.port=8080
school.max-score=100
src/main/resources/application.properties — uygulamanın ayarları

Ayarlar koda dokunmadan değiştirilebilir: java -jar school.jar --server.port=9090 ya da SERVER_PORT ortam değişkeniyle. Farklı ortamlar için profiller vardır: örneğin application-dev.properties dosyası --spring.profiles.active=dev ile etkinleşir.

curl ile test

Sunucu çalışırken ikinci bir terminal aç. curl, HTTP istekleri gönderen bir komut satırı aracıdır: -X metodu, -H bir başlığı, -d ise gövdeyi belirler. Önce iki öğrenci oluşturup listeyi okuyalım:

Terminal
curl -X POST localhost:8080/api/students -H 'Content-Type: application/json' -d '{"name":"Aysel","score":95}'
curl -X POST localhost:8080/api/students -H 'Content-Type: application/json' -d '{"name":"Murad","score":78}'
curl localhost:8080/api/students
curl localhost:8080/api/students/1
Beklenen çıktı
{"id":1,"name":"Aysel","score":95}
{"id":2,"name":"Murad","score":78}
[{"id":1,"name":"Aysel","score":95},{"id":2,"name":"Murad","score":78}]
{"id":1,"name":"Aysel","score":95}
Örnek çıktı: JSON alanları record'daki sırayla gelir, id değerini sunucu verir

Şimdi güncelleme, silme ve hata durumları. -w '%{http_code}' yalnızca durum kodunu yazdırır, -s -o /dev/null ise gövdeyi ve ilerleme bilgisini gizler:

Terminal
curl -X PUT localhost:8080/api/students/2 -H 'Content-Type: application/json' -d '{"name":"Murad","score":84}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE localhost:8080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/students \
     -H 'Content-Type: application/json' -d '{"name":"Elvin","score":120}'
Beklenen çıktı
{"id":2,"name":"Murad","score":84}
204
404
400
Örnek çıktı: 204 — silindi, 404 — artık yok, 400 — 120 puan kabul edilmez
Java
@SpringBootTest
@AutoConfigureMockMvc
class StudentControllerTest {

    @Autowired
    MockMvc mvc;

    @Test
    void createsStudent() throws Exception {
        mvc.perform(post("/api/students")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"name\":\"Aysel\",\"score\":95}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.name").value("Aysel"));
    }
}
Otomatik test: MockMvc, gerçek sunucuyu başlatmadan bir HTTP isteğini taklit eder (içe aktarmalar gösterilmedi)

Önemli noktalar

  • Spring Boot: @SpringBootApplication + otomatik yapılandırma + gömülü Tomcat; çalıştırmak için ./mvnw spring-boot:run.
  • Katmanlar: @RestController → @Service → @Repository; Spring bean'leri oluşturur ve yapıcılara verir.
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PathVariable, @RequestBody; JSON dönüşümünü Jackson yapar.
  • Doğru durum kodları: 200, 201, 204, 400, 404; @ResponseStatus ve ResponseStatusException ile.
  • Ayarlar application.properties içinde tutulur ve @Value ile okunur; API curl ya da MockMvc ile test edilir.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
HTTP isteklerini alıp JSON döndüren bir sınıf hangi ek açıklamayla işaretlenir?