Kurs JavaScript i React · Moduł 11: Pobieranie danych i API
POST, PUT, DELETE - Wysyłanie danych do serwera
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óbRóż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. Dlaczego w zapytaniu POST używamy JSON.stringify() do przekazania danych w body?
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: