Kurs JavaScript i React · Moduł 11: Pobieranie danych i API

POST, PUT, DELETE - Wysyłanie danych do serwera

5 min czytania
W tej lekcji6

Do tej pory tylko słuchaliśmy Centrum Dowodzenia: pobieraliśmy dane metodą GET. Ale załoga chce też coś zgłosić: zarejestrować nową misję, poprawić jej cel albo ją odwołać. Sam GET tego nie zrobi. Teraz nauczymy się wysyłać dane do serwera: tworzyć nowe zasoby (POST), aktualizować istniejące (PUT) i usuwać je (DELETE). To jak wysyłanie rozkazów z naszego statku kosmicznego do Centrum Dowodzenia.

Metody HTTP

Metoda HTTP mówi serwerowi, co chcesz zrobić z zasobem pod danym adresem. Ten sam adres /missions/7 może oznaczać "pokaż", "zmień" albo "usuń", zależnie od metody:

1// GET    - Pobierz dane (domyślna metoda fetch)
2// POST   - Utwórz nowy zasób
3// PUT    - Zaktualizuj cały zasób
4// PATCH  - Zaktualizuj część zasobu
5// DELETE - Usuń zasób

Różnica między PUT a PATCH jest ważna: PUT wysyła cały nowy obiekt i zastępuje nim stary, PATCH wysyła tylko zmienione pola. Jeśli w PUT pominiesz pole, serwer zgodny z tą konwencją może je wyczyścić.

Zapytanie POST - tworzenie nowych zasobów

Domyślnie fetch() wysyła GET. Żeby wysłać dane, przekazujemy drugi argument, obiekt opcji, z trzema polami: method (metoda), headers (nagłówki) i body (treść zapytania):

1async function createMission(missionData) {
2  const response = await fetch(
3    'https://api.space-center.com/missions',
4    {
5      method: 'POST',
6      headers: {
7        'Content-Type': 'application/json',
8      },
9      body: JSON.stringify(missionData),
10    }
11  );
12
13  if (!response.ok) {
14    throw new Error('Nie udało się utworzyć misji');
15  }
16
17  const newMission = await response.json();
18  return newMission;
19}
20
21// Użycie:
22const mission = await createMission({
23  name: 'Alpha Centauri Exploration',
24  crew: ['Kapitan Nova', 'Dr. Astro'],
25  priority: 'high',
26});

Dlaczego JSON.stringify? Pole body przyjmuje tekst, FormData, Blob i kilka innych typów, ale nie serializuje zwykłego obiektu JavaScript. Gdybyś podał obiekt bezpośrednio, poleciałby napis "[object Object]". Sprawdzanie response.ok zostaje takie samo jak przy GET, bo fetch() nie rzuca błędów dla 4xx/5xx także przy POST.

Nagłówki zapytania (Headers)

Nagłówek Content-Type informuje serwer, w jakim formacie wysyłamy dane. Bez niego serwer może nie rozpoznać JSON-a i odrzucić zapytanie:

1// JSON - najczęściej używany format
2headers: {
3  'Content-Type': 'application/json',
4}
5
6// Dane formularza
7headers: {
8  'Content-Type': 'application/x-www-form-urlencoded',
9}
10
11// Autoryzacja - token dostępu
12headers: {
13  'Content-Type': 'application/json',
14  'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
15}

Nie myl go z nagłówkiem Accept: on mówi, jaki format chcesz dostać w odpowiedzi. Jedna pułapka: gdy wysyłasz FormData (np. z plikiem), nie ustawiaj Content-Type ręcznie, bo przeglądarka sama doda go razem z granicą części (boundary).

Formularz z POST w React

W prawdziwej aplikacji dane do POST pochodzą z formularza. Potrzebujemy stanów na pola, flagi submitting (żeby zablokować przycisk) oraz stanów na wynik i błąd. Najpierw logika wysyłania:

1import React, { useState } from 'react';
2
3function MissionForm() {
4  const [name, setName] = useState('');
5  const [destination, setDestination] = useState('');
6  const [submitting, setSubmitting] = useState(false);
7  const [result, setResult] = useState(null);
8  const [error, setError] = useState(null);
9
10  const handleSubmit = async (e) => {
11    e.preventDefault();
12    setSubmitting(true);
13    setError(null);
14
15    try {
16      const response = await fetch(
17        'https://api.space-center.com/missions',
18        {
19          method: 'POST',
20          headers: { 'Content-Type': 'application/json' },
21          body: JSON.stringify({ name, destination }),
22        }
23      );
24
25      if (!response.ok) {
26        throw new Error(`Błąd: ${response.status}`);
27      }
28
29      const data = await response.json();
30      setResult(data);
31      setName('');
32      setDestination('');
33    } catch (err) {
34      setError(err.message);
35    } finally {
36      setSubmitting(false);
37    }
38  };

e.preventDefault() zatrzymuje domyślne przeładowanie strony przez formularz. Pola czyścimy dopiero po sukcesie, więc przy błędzie użytkownik nie traci wpisanych danych. Druga część tego samego komponentu to widok:

1  return (
2    <form onSubmit={handleSubmit}>
3      <input
4        value={name}
5        onChange={e => setName(e.target.value)}
6        placeholder="Nazwa misji"
7        required
8      />
9      <input
10        value={destination}
11        onChange={e => setDestination(e.target.value)}
12        placeholder="Cel podróży"
13        required
14      />
15      <button type="submit" disabled={submitting}>
16        {submitting ? 'Tworzenie misji...' : 'Utwórz misję'}
17      </button>
18
19      {error && <p className="error">{error}</p>}
20      {result && (
21        <p className="success">
22          Misja "{result.name}" utworzona! ID: {result.id}
23        </p>
24      )}
25    </form>
26  );
27}

disabled={submitting} chroni przed podwójnym kliknięciem, które wysłałoby dwie identyczne misje. React 19 dodał też Actions (useActionState, prop action na <form>), które przejmują część tej pracy ze stanem, ale samo zapytanie HTTP wygląda dokładnie tak samo.

PUT - Aktualizacja zasobu

Aktualizacja wygląda prawie jak tworzenie. Zmieniają się dwie rzeczy: metoda na PUT i adres, który teraz wskazuje konkretną misję przez jej id:

1async function updateMission(id, updatedData) {
2  const response = await fetch(
3    `https://api.space-center.com/missions/${id}`,
4    {
5      method: 'PUT',
6      headers: { 'Content-Type': 'application/json' },
7      body: JSON.stringify(updatedData),
8    }
9  );
10
11  if (!response.ok) {
12    throw new Error('Aktualizacja nie powiodła się');
13  }
14
15  return await response.json();
16}

Nagłówek i JSON.stringify pozostały bez zmian. Pamiętaj tylko, że updatedData powinno zawierać cały obiekt misji.

DELETE - Usuwanie zasobu

DELETE jest najprostszy, bo zwykle nie wysyła żadnej treści: wystarczy metoda i adres. Serwer często odpowiada kodem 204 No Content, czyli "sukces, ale bez treści", więc nie wołamy wtedy response.json():

1async function deleteMission(id) {
2  const response = await fetch(
3    `https://api.space-center.com/missions/${id}`,
4    {
5      method: 'DELETE',
6    }
7  );
8
9  if (!response.ok) {
10    throw new Error('Usuwanie nie powiodło się');
11  }
12
13  // DELETE często zwraca 204 No Content (brak body)
14  return response.status === 204;
15}

Wywołanie response.json() na pustej odpowiedzi 204 rzuciłoby błąd parsowania, dlatego zwracamy tylko informację o statusie. Teraz podłączmy tę funkcję do przycisku w komponencie:

1// W komponencie React
2function MissionItem({ mission, onDelete }) {
3  const [deleting, setDeleting] = useState(false);
4
5  const handleDelete = async () => {
6    if (!window.confirm('Na pewno usunąć misję?')) return;
7
8    setDeleting(true);
9    try {
10      await deleteMission(mission.id);
11      onDelete(mission.id);
12    } catch (err) {
13      alert('Błąd: ' + err.message);
14    } finally {
15      setDeleting(false);
16    }
17  };
18
19  return (
20    <div>
21      <h3>{mission.name}</h3>
22      <button onClick={handleDelete} disabled={deleting}>
23        {deleting ? 'Usuwanie...' : 'Usuń misję'}
24      </button>
25    </div>
26  );
27}

Komponent pyta o potwierdzenie, bo usunięcia nie da się cofnąć, a listę aktualizuje rodzic przez onDelete dopiero po sukcesie serwera. Polecam tę kolejność na start. W lekcji o cachowaniu poznasz odważniejszy wariant: optimistic updates, które zmieniają UI jeszcze przed odpowiedzią.

Pamiętaj: GET to nasłuch, a POST, PUT i DELETE to rozkazy wysyłane do Centrum Dowodzenia, każdy z właściwą metodą, nagłówkiem i sprawdzeniem response.ok.

Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2
3// Symulacja serwera API
4const mockDatabase = {
5  missions: [
6    { id: 1, name: "Alpha Mission", destination: "Mars", status: "active" },
7    { id: 2, name: "Beta Recon", destination: "Europa", status: "active" },
8    { id: 3, name: "Gamma Survey", destination: "Titan", status: "completed" },
9  ],
10  nextId: 4,
11};
12
13function fakeFetchApi(url, options = {}) {
14  return new Promise((resolve, reject) => {
15    setTimeout(() => {
16      const method = (options.method || 'GET').toUpperCase();
17
18      if (method === 'GET') {
19        resolve({
20          ok: true,
21          json: () => Promise.resolve({ missions: [...mockDatabase.missions] }),
22        });
23      } else if (method === 'POST') {
24        const body = JSON.parse(options.body);
25        const newMission = { ...body, id: mockDatabase.nextId++, status: 'active' };
26        mockDatabase.missions.push(newMission);
27        resolve({
28          ok: true,
29          status: 201,
30          json: () => Promise.resolve(newMission),
31        });
32      } else if (method === 'DELETE') {
33        const id = parseInt(url.split('/').pop());
34        mockDatabase.missions = mockDatabase.missions.filter(m => m.id !== id);
35        resolve({ ok: true, status: 204 });
36      }
37    }, 800);
38  });
39}
40
41function MissionControl() {
42  const [missions, setMissions] = useState([]);
43  const [name, setName] = useState('');
44  const [destination, setDestination] = useState('');
45  const [submitting, setSubmitting] = useState(false);
46  const [loading, setLoading] = useState(false);
47  const [message, setMessage] = useState(null);
48
49  const loadMissions = async () => {
50    setLoading(true);
51    const response = await fakeFetchApi('/api/missions');
52    const data = await response.json();
53    setMissions(data.missions);
54    setLoading(false);
55  };
56
57  const createMission = async (e) => {
58    e.preventDefault();
59    if (!name || !destination) return;
60    setSubmitting(true);
61    setMessage(null);
62
63    try {
64      const response = await fakeFetchApi('/api/missions', {
65        method: 'POST',
66        headers: { 'Content-Type': 'application/json' },
67        body: JSON.stringify({ name, destination }),
68      });
69      const newMission = await response.json();
70      setMissions(prev => [...prev, newMission]);
71      setName('');
72      setDestination('');
73      setMessage({ type: 'success', text: `Misja "${newMission.name}" utworzona!` });
74    } catch (err) {
75      setMessage({ type: 'error', text: err.message });
76    } finally {
77      setSubmitting(false);
78    }
79  };
80
81  const deleteMission = async (id) => {
82    try {
83      await fakeFetchApi(`/api/missions/${id}`, { method: 'DELETE' });
84      setMissions(prev => prev.filter(m => m.id !== id));
85      setMessage({ type: 'success', text: 'Misja usunieta' });
86    } catch (err) {
87      setMessage({ type: 'error', text: err.message });
88    }
89  };
90
91  React.useEffect(() => { loadMissions(); }, []);
92
93  return (
94    <div className="control">
95      <h1>Centrum Dowodzenia Misjami</h1>
96
97      <form className="mission-form" onSubmit={createMission}>
98        <h3>Nowa Misja (POST)</h3>
99        <input value={name} onChange={e => setName(e.target.value)} placeholder="Nazwa misji" />
100        <input value={destination} onChange={e => setDestination(e.target.value)} placeholder="Cel podrozy" />
101        <button type="submit" disabled={submitting}>
102          {submitting ? 'Tworzenie...' : 'Utworz misje (POST)'}
103        </button>
104      </form>
105
106      {message && (
107        <div className={"message " + message.type}>{message.text}</div>
108      )}
109
110      <h3>Aktywne misje ({missions.length})</h3>
111      {loading ? <p>Ladowanie...</p> : (
112        <div className="mission-list">
113          {missions.map(m => (
114            <div key={m.id} className="mission-card">
115              <div>
116                <h4>{m.name}</h4>
117                <p>Cel: {m.destination} | Status: {m.status}</p>
118              </div>
119              <button className="delete-btn" onClick={() => deleteMission(m.id)}>
120                Usun (DELETE)
121              </button>
122            </div>
123          ))}
124        </div>
125      )}
126    </div>
127  );
128}
129
130export default MissionControl;

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Dlaczego w zapytaniu POST używamy JSON.stringify() do przekazania danych w body?

  2. 2. Który nagłówek HTTP informuje serwer, że wysyłamy dane w formacie JSON?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Ułóż kroki cyklu żądanie-odpowiedź HTTP:

  • Edytor kodu

    Formularz z zapytaniem POST

  • Edytor kodu

    Operacje CRUD

  • Układanie w pionie

    Ułóż składnię wywołania fetch z metodą POST i nagłówkiem Content-Type:

Przydatne artykuły