React + REST API

React'in bir REST API'ye bağlanması -- API katmanını bir api.js modülünde ayırmak, listeleme, oluşturma ve silme işlemlerini birlikte yönetmek, basit örneklerle.

Orta 15 dk
EN

React + REST API

Fetching Data dersinde fetch'in GET/POST/PUT/DELETE'ini tek tek gördük. Bu ders, bunları bir araya getirip GERÇEK bir uygulamada kurslar listesi gibi bir kaynağı uçtan uca (listeleme, ekleme, silme) yönetmeyi anlatıyor -- ayrıca React'in bir backend'le nasıl konuştuğunu büyük resmiyle gösteriyor.

React → HTTP → Backend → Veritabanı

Bir React uygulaması, verisini genellikle kendi başına saklamaz -- fetch ile bir backend'e (bu projenin kendisi gibi bir Spring Boot uygulaması olabilir) HTTP isteği atar. Örneğin bir kurs listesi istendiğinde: React fetch("/courses") çağırır → istek HTTP üzerinden backend'e ulaşır → backend'in bir @RestController'ı (bu terimi Java kursundaki REST API Tasarımı dersinden hatırlayabilirsin) isteği karşılar → veriyi veritabanından (PostgreSQL) okur → JSON olarak geri döner → React bu JSON'ı state'e yazıp ekranı günceller. Bu derste gerçek bir Spring Boot backend'i kurmuyoruz -- onun yerine aynı REST kurallarına (GET/POST/DELETE, JSON, HTTP durum kodları) uyan basit bir sahte sunucu (json-server) kullanıyoruz; React tarafındaki kod, gerçek bir Spring Boot API'sine bağlanırken de birebir aynı olurdu.

API Katmanını Ayırmak: Bir api.js Modülü

Her component'in içine fetch çağrıları dağıtmak yerine, bunları TEK bir yerde toplamak yaygın bir pratiktir:

// Gerçek uygulamalarda, fetch çağrılarını her component'in içine dağıtmak
// yerine ayrı bir dosyada (örneğin api.js) toplamak yaygındır -- bu, tüm
// istekleri TEK bir yerde tutar, `BASE_URL`'i tek bir yerde değiştirmeyi
// sağlar. Bu örnek, o dosyanın İÇERİĞİNİ gösteriyor (normalde ayrı bir
// dosya olurdu, component'ler bu fonksiyonları import ederdi).

const BASE_URL = "http://localhost:3000";

async function getCourses() {
  const response = await fetch(`${BASE_URL}/courses`);
  if (!response.ok) {
    throw new Error("Failed to load courses");
  }
  return response.json();
}

async function createCourse(course) {
  const response = await fetch(`${BASE_URL}/courses`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(course),
  });
  return response.json();
}

async function deleteCourse(courseId) {
  await fetch(`${BASE_URL}/courses/${courseId}`, { method: "DELETE" });
}

function ApiModuleExample() {
  return (
    <p>
      This represents an api.js module: getCourses(), createCourse() and
      deleteCourse() wrap the raw fetch calls in one place.
    </p>
  );
}

getCourses(), createCourse(), deleteCourse() gibi fonksiyonlar, fetch'in ayrıntılarını (URL, method, headers) SAKLAR -- component'ler yalnızca bu fonksiyonları çağırır, fetch'in kendisiyle hiç uğraşmaz. BASE_URL'i tek bir yerde tanımlamak, backend adresi değiştiğinde (örneğin geliştirmeden production'a geçerken) tek bir satırı değiştirmeyi yeterli kılar.

Backend'den Kurs Listesini Çekmek

useEffect içinde async/await ile veri çekmek, .then() zincirine göre daha okunaklı bir alternatif:

import { useEffect, useState } from "react";

const BASE_URL = "http://localhost:3000";

function FetchCoursesExample() {
  const [courses, setCourses] = useState([]);

  useEffect(() => {
    // useEffect'in callback'i doğrudan async OLAMAZ -- bu yüzden içeride
    // ayrı bir async fonksiyon tanımlayıp hemen çağırıyoruz. Gerçek bir
    // projede burası (Spring Boot ile) GET /api/courses'a karşılık gelir --
    // React, HTTP üzerinden backend'e istek atar, backend veritabanından
    // (PostgreSQL) okuyup JSON döner.
    async function loadCourses() {
      const response = await fetch(`${BASE_URL}/courses`);
      const data = await response.json();
      setCourses(data);
    }

    loadCourses();
  }, []);

  return (
    <ul>
      {courses.map((course) => (
        <li key={course.id}>{course.name}</li>
      ))}
    </ul>
  );
}

useEffect'in callback'i doğrudan async OLAMAZ (React bunu desteklemiyor) -- bu yüzden içeride ayrı bir async fonksiyon tanımlayıp hemen çağırıyoruz. await, fetch'in ve response.json()'ın Promise'lerinin tamamlanmasını BEKLER; sonuç geldiğinde setCourses ile state güncellenir.

Yeni Bir Kurs Oluşturmak

Forms dersindeki controlled input + onSubmit deseni, bir POST isteğiyle birleşiyor:

import { useState } from "react";

const BASE_URL = "http://localhost:3000";

function CreateCourseFormExample({ onCreated }) {
  const [name, setName] = useState("");

  async function handleSubmit(event) {
    event.preventDefault();

    // Forms dersindeki controlled input + onSubmit deseni, burada bir POST
    // isteğiyle birleşiyor. Spring Boot tarafında bu, `@RequestBody` ile
    // JSON'ı bir Java nesnesine çeviren bir `@PostMapping` metoduna karşılık
    // gelir (bkz. REST API Tasarımı dersi, Java kursunda).
    const response = await fetch(`${BASE_URL}/courses`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name }),
    });
    const newCourse = await response.json();

    onCreated(newCourse);
    setName("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(event) => setName(event.target.value)}
        placeholder="Course name"
      />
      <button type="submit">Add Course</button>
    </form>
  );
}

Form gönderildiğinde, girilen değer POST isteğiyle backend'e gönderilir; backend yeni kaydı oluşturup (genellikle kendi ürettiği bir id ile) geri döner. onCreated(newCourse), bu yeni kaydı ÜST component'e bildirmek için kullanılan bir callback prop -- Props dersindeki "veriyi yukarı iletmek" deseni.

Bir Kursu Silmek

Bir kaydı sunucudan sildikten sonra, ekrandaki listeyi de güncellememiz gerekir:

const BASE_URL = "http://localhost:3000";

function DeleteCourseExample({ courses, setCourses }) {
  async function handleDelete(courseId) {
    await fetch(`${BASE_URL}/courses/${courseId}`, { method: "DELETE" });

    // Sunucudan sildikten SONRA, ekrandaki listeyi de güncellememiz gerekir
    // -- State dersindeki immutability kuralına uyarak, silinen kaydı
    // filter() ile ÇIKARIP yeni bir dizi oluşturuyoruz.
    setCourses(courses.filter((course) => course.id !== courseId));
  }

  return (
    <ul>
      {courses.map((course) => (
        <li key={course.id}>
          {course.name}
          <button onClick={() => handleDelete(course.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

DELETE isteği başarılı olduktan SONRA, State dersindeki immutability kuralına uyarak filter() ile silinen kaydı çıkarıp YENİ bir dizi oluşturuyoruz -- diziyi doğrudan değiştirmiyoruz (splice gibi).

Oluşturduktan Sonra Ekranı Güncellemek

Yeni bir kayıt oluşturduktan sonra, listeyi baştan tekrar çekmek yerine (bir istek daha atmak yerine) genellikle yeni kaydı doğrudan mevcut listeye eklemek tercih edilir:

import { useEffect, useState } from "react";

const BASE_URL = "http://localhost:3000";

function RefreshAfterMutationExample() {
  const [courses, setCourses] = useState([]);
  const [name, setName] = useState("");

  useEffect(() => {
    fetch(`${BASE_URL}/courses`)
      .then((response) => response.json())
      .then((data) => setCourses(data));
  }, []);

  async function handleSubmit(event) {
    event.preventDefault();

    const response = await fetch(`${BASE_URL}/courses`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name }),
    });
    const newCourse = await response.json();

    // Sunucu yeni kaydı oluşturduktan sonra, listeyi baştan çekmek yerine
    // (bir istek daha atmak yerine) State dersindeki immutability kuralına
    // uyarak yeni kaydı doğrudan mevcut listeye ekliyoruz -- ekran anında
    // güncellenir.
    setCourses([...courses, newCourse]);
    setName("");
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={name}
          onChange={(event) => setName(event.target.value)}
          placeholder="Course name"
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {courses.map((course) => (
          <li key={course.id}>{course.name}</li>
        ))}
      </ul>
    </div>
  );
}

setCourses([...courses, newCourse]), State dersindeki spread deseniyle eski listeyi kopyalayıp sonuna yeni kaydı ekliyor -- ekran, sunucuya ikinci bir istek atmadan ANINDA güncelleniyor.

Özet ve Terimler Sözlüğü

Bir React uygulaması, fetch ile bir backend'e (Spring Boot gibi) HTTP istekleri atarak veri okur ve yazar; fetch çağrılarını ayrı bir api.js modülünde toplamak yaygın bir pratiktir. Veri çekme genellikle useEffect içinde async/await ile yapılır. Bir kayıt oluşturulduğunda ya da silindiğinde, ekrandaki listeyi State dersindeki immutability kurallarına (filter(), spread) uyarak güncellemek gerekir -- bazen sunucuya ikinci bir istek atmak yerine, elimizdeki sonucu doğrudan state'e yansıtmak yeterlidir.

Terimler Sözlüğü

REST API — Kaynakları (örneğin kurslar) HTTP metodlarıyla (GET/POST/PUT/DELETE) yöneten bir backend arayüzü tasarım biçimi.

api.js Modülü — Bir uygulamadaki tüm fetch çağrılarını tek bir yerde toplayan, component'lerin doğrudan kullandığı yardımcı fonksiyon dosyası.

Pratik Proje

Bu kategoride (Fetching Data, React + REST API) öğrendiğimiz kavramları bir arada kullanan, gerçek ve çalıştırılabilir bir örnek proje var: API & Data Fetching Demo -- json-server ile çalışan sahte bir REST API'ye bağlanan bir kurs listesi uygulaması.

Proje; useEffect + fetch ile listeleme, loading/error state yönetimi, controlled form ile yeni kayıt oluşturma (POST), bir kaydı silme (DELETE), ve her mutasyondan sonra ekranı immutability kurallarına uyarak güncellemeyi bir arada gösteriyor. Bilgisayarına indirip çalıştırabilir, kodunu satır satır inceleyebilirsin:

git clone https://github.com/cdurgun/react-course-projects.git
cd react-course-projects
npm install
cd projects/api-data-fetching
npm run server

Sahte API'yi (json-server) ayrı bir terminalde çalıştırdıktan sonra, başka bir terminalde React uygulamasını başlat:

cd react-course-projects/projects/api-data-fetching
npm run dev

react-course-projects deposu npm workspaces kullanır -- npm install yalnızca bir kez, depo kökünde çalıştırılır ve tüm proje klasörleri ortak bağımlılıkları paylaşır (her klasörde ayrı bir node_modules oluşmaz). Kök dizinde npm install'ı daha önce çalıştırdıysan, doğrudan cd react-course-projects/projects/api-data-fetching yapıp yukarıdaki iki komutu (npm run server, npm run dev) iki ayrı terminalde çalıştırman yeterli.