Temat 31 · 3 × 45 min
REST, CRUD i wysyłanie formularzy
W lekcji async/await i fetch pobierałeś dane z serwera. Prawdziwa aplikacja musi je też wysyłać: dodać zadanie do listy, zapisać komentarz, usunąć produkt z koszyka. Serwery udostępniają takie operacje jako REST API – zestaw adresów i metod HTTP, które działają tak samo niezależnie od języka po drugiej stronie (PHP, Node, Python). W tej lekcji nauczysz się rozmawiać z takim API z przeglądarki: wysyłać JSON, obsługiwać formularz bez przeładowania strony, postawić własny testowy serwer i połączyć fetch z PHP, które znasz z części serwerowej INF.03.
Czym jest REST: zasoby, adresy, metody
REST (Representational State Transfer) to umowa, jak projektować API nad HTTP. Trzy zasady wystarczą ci na start:
- Wszystko jest zasobem (resource) i ma adres:
/zadania,/zadania/7,/uzytkownicy/3/zamowienia. Adres mówi co, nie co zrobić – nie ma/usunZadanie.php?id=7. - Metoda HTTP mówi, co robisz z zasobem. Te same cztery operacje, które znasz z baz danych jako CRUD (Create, Read, Update, Delete), mają swoje metody.
- Dane podróżują w obie strony jako JSON, a wynik operacji opisuje kod statusu.
| Operacja | Metoda | Adres | Ciało żądania | Typowa odpowiedź |
|---|---|---|---|---|
| Read – lista | GET | /zadania | brak | 200 + tablica |
| Read – jeden | GET | /zadania/7 | brak | 200 + obiekt albo 404 |
| Create | POST | /zadania | nowy obiekt (bez id) | 201 + obiekt z nadanym id |
| Update – cały | PUT | /zadania/7 | pełny obiekt | 200 + obiekt |
| Update – część | PATCH | /zadania/7 | tylko zmienione pola | 200 + obiekt |
| Delete | DELETE | /zadania/7 | brak | 200 z obiektem albo 204 bez treści |
Różnica między PUT a PATCH jest praktyczna: PUT wysyłasz, gdy masz cały zaktualizowany obiekt (np. po edycji formularza), PATCH – gdy zmieniasz jedno pole (np. odhaczasz zadanie jako wykonane). GET i DELETE nie mają ciała – parametry przekazujesz w adresie: /zadania?wykonane=true.
Użytkownik kliknął „Wykonane” przy zadaniu o id 12. Które żądanie najlepiej opisuje tę operację w stylu REST?
Kody statusu: co odpowiedział serwer
Kod statusu to trzycyfrowa liczba w odpowiedzi. Pierwsza cyfra mówi, jaka to kategoria. W kodzie masz ją pod response.status, a response.ok to skrót na „status od 200 do 299”.
| Kod | Znaczenie | Kiedy go zobaczysz | Co zrobić w JS |
|---|---|---|---|
200 OK | udało się | GET, PUT, PATCH, czasem DELETE | czytaj response.json() |
201 Created | utworzono zasób | po POST | odczytaj nadane id z odpowiedzi |
204 No Content | udało się, brak treści | po DELETE | nie wywołuj response.json() – treść jest pusta |
400 Bad Request | zły format danych | brakuje pola, zły JSON | pokaż błąd walidacji użytkownikowi |
401 Unauthorized | niezalogowany | brak tokenu / sesji | przekieruj do logowania |
403 Forbidden | zalogowany, ale bez uprawnień | cudzy zasób | komunikat „brak dostępu” |
404 Not Found | nie ma takiego adresu/zasobu | literówka w URL, usunięty rekord | komunikat; fetch nie rzuca błędu! |
500 Internal Server Error | błąd po stronie serwera | bug w PHP, padła baza | komunikat „spróbuj później”, zgłoś |
Co wypisze ten kod, jeśli serwer odpowie statusem 404 i pustym ciałem?
const res = await fetch('/zadania/999');
console.log(res.ok, res.status);Funkcja pomocnicza, którą warto mieć w każdym projekcie – zamienia odpowiedź na dane albo rzuca czytelny błąd:
async function pobierzJson(response) {
if (!response.ok) {
throw new Error(`Serwer odpowiedział ${response.status}`);
}
// 204 No Content – nie ma czego parsować
if (response.status === 204) return null;
return response.json();
}
// symulacja trzech odpowiedzi (prawdziwe zwróci fetch)
const odpowiedzi = [
{ ok: true, status: 200, json: async () => ({ id: 1, tytul: 'Nauka' }) },
{ ok: true, status: 204, json: async () => { throw new Error('puste ciało'); } },
{ ok: false, status: 404, json: async () => ({}) },
];
async function sprawdzWszystkie() {
for (const res of odpowiedzi) {
try {
console.log(res.status, '→', await pobierzJson(res));
} catch (e) {
console.log(res.status, '→ błąd:', e.message);
}
}
}
sprawdzWszystkie();Żądanie z ciałem JSON
fetch(adres) to zawsze GET. Każdą inną metodę opisujesz drugim argumentem – obiektem opcji. Trzy rzeczy muszą się zgadzać:
const nowe = { tytul: 'Powtórzyć REST', wykonane: false };
const res = await fetch('http://localhost:3000/zadania', {
method: 'POST', // 1. metoda
headers: { 'Content-Type': 'application/json' }, // 2. mówisz serwerowi: to JSON
body: JSON.stringify(nowe), // 3. obiekt → napis JSON
});
const zapisane = await res.json(); // serwer odsyła obiekt z id
console.log(zapisane.id);Programista zapomniał o JSON.stringify i wysłał body: nowe. Co dostanie serwer jako treść żądania?
fetch('/zadania', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: nowe,
});Warto zamknąć powtarzający się kod w jedną funkcję. Poniżej wersja, którą będziesz używać przez resztę lekcji (w zadaniach też):
const BAZA = 'http://localhost:3000';
function zbudujOpcje(metoda, dane) {
const opcje = { method: metoda, headers: {} };
if (dane !== undefined) {
opcje.headers['Content-Type'] = 'application/json';
opcje.body = JSON.stringify(dane);
}
return opcje;
}
async function zapytanie(sciezka, metoda = 'GET', dane) {
const res = await fetch(BAZA + sciezka, zbudujOpcje(metoda, dane));
if (!res.ok) throw new Error(`${metoda} ${sciezka}: ${res.status}`);
return res.status === 204 ? null : res.json();
}
// cztery operacje CRUD w jednej linii każda:
const api = {
lista: () => zapytanie('/zadania'),
dodaj: (zadanie) => zapytanie('/zadania', 'POST', zadanie),
zmien: (id, zmiany) => zapytanie(`/zadania/${id}`, 'PATCH', zmiany),
usun: (id) => zapytanie(`/zadania/${id}`, 'DELETE'),
};
// podgląd opcji, które wyjdą do serwera:
console.log(zbudujOpcje('GET'));
console.log(zbudujOpcje('PATCH', { wykonane: true }));
// bez uruchomionego json-server (o nim niżej) żądanie się nie uda – zobaczysz komunikat z catch;
// prawdziwe dane zobaczysz w placu zabaw niżej
api
.lista()
.then((dane) => console.log('lista:', dane))
.catch((e) => console.log('Brak serwera na localhost:3000?', e.message));Plac zabaw: CRUD na JSONPlaceholder
Zanim postawisz własny serwer, poćwicz na publicznym fałszywym API JSONPlaceholder. Ma zasoby /posts (100), /comments (500), /todos (200), /users (10) i przyjmuje wszystkie metody. Uwaga: odpowiedzi na POST/PUT/PATCH/DELETE są udawane – serwer odsyła to, co by zrobił (np. nowy post dostaje id: 101), ale nic nie zapisuje. Do nauki to idealne: nie zepsujesz niczego.
<p>
<button id="btn-lista">GET /todos (3 pierwsze)</button>
<button id="btn-dodaj">POST /todos</button>
<button id="btn-zmien">PATCH /todos/1</button>
<button id="btn-usun">DELETE /todos/1</button>
</p>
<pre id="wynik">Kliknij przycisk…</pre>button { margin: 2px 4px 2px 0; padding: 6px 10px; }
pre { background: #f3f3f3; padding: 10px; min-height: 120px; white-space: pre-wrap; }const BAZA = 'https://jsonplaceholder.typicode.com';
const wynik = document.getElementById('wynik');
async function zapytanie(sciezka, metoda = 'GET', dane) {
const opcje = { method: metoda, headers: {} };
if (dane !== undefined) {
opcje.headers['Content-Type'] = 'application/json; charset=UTF-8';
opcje.body = JSON.stringify(dane);
}
const res = await fetch(BAZA + sciezka, opcje);
if (!res.ok) throw new Error(`${metoda} ${sciezka}: ${res.status}`);
return res.status === 204 ? null : res.json();
}
function pokaz(opis, dane) {
wynik.textContent = `${opis}\n\n${JSON.stringify(dane, null, 2)}`;
}
async function obsluz(opis, akcja) {
wynik.textContent = 'Czekam na serwer…';
try {
pokaz(opis, await akcja());
} catch (e) {
wynik.textContent = 'Błąd: ' + e.message;
}
}
document.getElementById('btn-lista').addEventListener('click', () =>
obsluz('GET /todos?_limit=3', () => zapytanie('/todos?_limit=3')));
document.getElementById('btn-dodaj').addEventListener('click', () =>
obsluz('POST /todos → serwer nadaje id (udawane: 201)', () =>
zapytanie('/todos', 'POST', { title: 'Nauczyć się REST', completed: false, userId: 1 })));
document.getElementById('btn-zmien').addEventListener('click', () =>
obsluz('PATCH /todos/1 → tylko pole completed', () =>
zapytanie('/todos/1', 'PATCH', { completed: true })));
document.getElementById('btn-usun').addEventListener('click', () =>
obsluz('DELETE /todos/1 → 200 i pusty obiekt', () => zapytanie('/todos/1', 'DELETE')));Zmień w edytorze '/todos?_limit=3' na '/todos/9999' w przycisku GET i zobacz, jak zachowa się zapytanie przy 404.
Formularz bez przeładowania strony
Formularz z method="post" po kliknięciu „Wyślij” przeładowuje stronę – przeglądarka sama wysyła dane i wyświetla odpowiedź serwera. W aplikacji JS chcesz przejąć ten moment: zatrzymać domyślną akcję, zebrać dane, wysłać je fetch i pokazać wynik bez migania strony. Schemat jest zawsze ten sam:
- nasłuchuj zdarzenia
submitna formularzu (nieclickna przycisku – wtedy działa też Enter i walidacja HTML5required), event.preventDefault(),- zbierz dane:
new FormData(form), - zablokuj przycisk na czas wysyłki, wyślij, pokaż status, odblokuj.
Co się stanie po kliknięciu „Wyślij”, jeśli w obsłudze submit zabraknie preventDefault()?
form.addEventListener('submit', async (e) => {
const res = await fetch('/api', { method: 'POST', body: new FormData(form) });
status.textContent = 'Zapisano';
});<form id="form-komentarz">
<label>Imię <input name="imie" required minlength="2"></label>
<label>Komentarz <textarea name="tresc" required rows="2"></textarea></label>
<button type="submit">Wyślij</button>
<span id="status" aria-live="polite"></span>
</form>
<ul id="lista"></ul>label { display: block; margin-bottom: 6px; }
input, textarea { display: block; width: 100%; max-width: 320px; }
#status { margin-left: 8px; color: #555; }
button[disabled] { opacity: 0.6; }const form = document.getElementById('form-komentarz');
const status = document.getElementById('status');
const lista = document.getElementById('lista');
form.addEventListener('submit', async (event) => {
event.preventDefault(); // 1. nie przeładowuj
const dane = Object.fromEntries(new FormData(form)); // 2. { imie: '…', tresc: '…' }
const przycisk = form.querySelector('button');
przycisk.disabled = true; // 3. blokada przed podwójnym kliknięciem
status.textContent = 'Wysyłam…';
try {
const res = await fetch('https://jsonplaceholder.typicode.com/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify({ name: dane.imie, body: dane.tresc, postId: 1 }),
});
if (!res.ok) throw new Error(`status ${res.status}`);
const zapisany = await res.json(); // serwer nadał id (tu: 501)
const li = document.createElement('li');
li.textContent = `#${zapisany.id} ${zapisany.name}: ${zapisany.body}`;
lista.prepend(li);
form.reset(); // 4. wyczyść pola
status.textContent = 'Zapisano.';
} catch (e) {
status.textContent = 'Nie udało się: ' + e.message;
} finally {
przycisk.disabled = false;
}
});Zwróć uwagę: pole imie z required minlength="2" jest sprawdzane przez przeglądarkę zanim odpali się submit. To za darmo dostajesz walidację z lekcji o walidacji.
FormData: dane formularza jako obiekt
FormData to wbudowana klasa, która zbiera pary nazwa → wartość ze wszystkich kontrolek formularza z atrybutem name (bez name pole jest pomijane). Masz dwa sposoby jej użycia:
| Chcesz | Zrób | Co leci do serwera |
|---|---|---|
| wysłać JSON | JSON.stringify(Object.fromEntries(new FormData(form))) + nagłówek Content-Type: application/json | {"imie":"Ola","tresc":"…"} |
wysłać plik lub do $_POST w PHP | body: new FormData(form) – bez ustawiania Content-Type | multipart/form-data jak klasyczny formularz |
const fd = new FormData(form); // z formularza…
fd.append('zrodlo', 'strona-www'); // …plus dodatkowe pole
fd.get('imie'); // 'Ola' – zawsze napis (albo File)
fd.has('tresc'); // true
for (const [nazwa, wartosc] of fd) console.log(nazwa, wartosc);
// checkboxy: obecne tylko gdy zaznaczone; kilka o tej samej nazwie → getAll
fd.getAll('zainteresowania'); // ['js', 'php']
Object.fromEntries(fd); // uwaga: z duplikatów zostaje ostatni!Własne API w minutę: json-server
json-server to narzędzie z npm, które z jednego pliku JSON robi pełne REST API z zapisem na dysk. Aktualna wersja to 1.0.0-beta.x (wymaga Node 22.12+). Potrzebujesz Node.js (lekcja o modułach i narzędziach opisuje instalację).
{
"zadania": [
{ "id": "1", "tytul": "Powtórzyć fetch", "wykonane": true },
{ "id": "2", "tytul": "Zrobić CRUD", "wykonane": false }
]
}npx json-server db.json
# JSON Server started on PORT :3000
# http://localhost:3000Od tej chwili masz pod http://localhost:3000/zadania wszystkie trasy z tabeli REST, a każdy POST/PATCH/DELETE zmienia plik db.json. Przydatne szczegóły wersji 1.x:
idjest napisem; przyPOSTbezidserwer sam je wygeneruje (losowy krótki napis),- filtrowanie i sortowanie w adresie:
/zadania?wykonane=true,/zadania?_sort=-id,/zadania?_page=1&_per_page=10, - serwer sam udostępnia pliki z folderu
./public– wrzuć tamindex.htmliapp.js, otwórzhttp://localhost:3000/i masz front i API pod jednym adresem (zero problemów z CORS), DELETEzwraca200i usunięty obiekt (nie204),--port 4000zmienia port;json-server --helppokaże resztę.
Pełna lista zadań z zapisem na serwerze – struktura, którą rozbudujesz w zadaniu 3:
<!doctype html>
<html lang="pl">
<head><meta charset="utf-8"><title>Zadania</title></head>
<body>
<form id="form-zadanie">
<input name="tytul" placeholder="Nowe zadanie" required>
<button>Dodaj</button>
</form>
<ul id="lista"></ul>
<p id="status"></p>
<script src="app.js" defer></script>
</body>
</html>const lista = document.getElementById('lista');
const status = document.getElementById('status');
const form = document.getElementById('form-zadanie');
async function zapytanie(sciezka, metoda = 'GET', dane) {
const opcje = { method: metoda, headers: {} };
if (dane !== undefined) {
opcje.headers['Content-Type'] = 'application/json';
opcje.body = JSON.stringify(dane);
}
const res = await fetch(sciezka, opcje); // ten sam adres co strona
if (!res.ok) throw new Error(`${metoda} ${sciezka}: ${res.status}`);
return res.status === 204 ? null : res.json();
}
function rysuj(zadania) {
lista.innerHTML = '';
for (const z of zadania) {
const li = document.createElement('li');
li.innerHTML = `
<label><input type="checkbox" data-id="${z.id}" ${z.wykonane ? 'checked' : ''}> ${z.tytul}</label>
<button data-usun="${z.id}">Usuń</button>`;
lista.append(li);
}
}
async function odswiez() {
try {
rysuj(await zapytanie('/zadania'));
status.textContent = '';
} catch (e) {
status.textContent = 'Błąd: ' + e.message;
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const { tytul } = Object.fromEntries(new FormData(form));
await zapytanie('/zadania', 'POST', { tytul, wykonane: false });
form.reset();
odswiez();
});
// delegacja: jeden nasłuchiwacz na całą listę (lekcja 27)
lista.addEventListener('change', async (e) => {
if (e.target.matches('input[type=checkbox]')) {
await zapytanie(`/zadania/${e.target.dataset.id}`, 'PATCH', { wykonane: e.target.checked });
}
});
lista.addEventListener('click', async (e) => {
const id = e.target.dataset.usun;
if (id) {
await zapytanie(`/zadania/${id}`, 'DELETE');
odswiez();
}
});
odswiez();Po każdej zmianie zajrzyj do db.json – zobaczysz, że serwer naprawdę zapisał dane. To ten moment, w którym „front-end” i „back-end” przestają być abstrakcją.
fetch do skryptu PHP na XAMPP
Z części serwerowej INF.03 znasz PHP i formularze z $_POST. fetch może gadać z PHP dokładnie tak samo – różnica jest tylko w tym, jak wysyłasz dane i co PHP odsyła.
Wariant A – FormData, PHP czyta $_POST jak zawsze:
const res = await fetch('zapisz.php', { method: 'POST', body: new FormData(form) });
const odp = await res.json();<?php
header('Content-Type: application/json; charset=utf-8'); // mówimy przeglądarce: odsyłam JSON
$imie = trim($_POST['imie'] ?? '');
$tresc = trim($_POST['tresc'] ?? '');
if ($imie === '' || $tresc === '') {
http_response_code(400); // zły format → 400
echo json_encode(['blad' => 'Wypełnij oba pola'], JSON_UNESCAPED_UNICODE);
exit;
}
// tu normalnie: INSERT do bazy przez mysqli, jak na INF.03
echo json_encode(['ok' => true, 'imie' => $imie, 'dlugosc' => mb_strlen($tresc)], JSON_UNESCAPED_UNICODE);Wariant B – ciało JSON, PHP czyta php://input:
<?php
header('Content-Type: application/json; charset=utf-8');
$surowe = file_get_contents('php://input'); // JSON NIE trafia do $_POST!
$dane = json_decode($surowe, true); // true → tablica asocjacyjna
if (!is_array($dane) || empty($dane['tytul'])) {
http_response_code(400);
echo json_encode(['blad' => 'Brak pola tytul']);
exit;
}
$zadanie = ['id' => rand(100, 999), 'tytul' => $dane['tytul'], 'wykonane' => false];
http_response_code(201); // utworzono
echo json_encode($zadanie, JSON_UNESCAPED_UNICODE);const res = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tytul: 'Nauka PHP + fetch' }),
});
console.log(res.status, await res.json()); // 201 { id: 537, tytul: '…', wykonane: false }Trzy rzeczy, o których trzeba pamiętać po stronie PHP:
header('Content-Type: application/json')przed jakimkolwiekecho– inaczej nagłówek się nie wyśle,- żadnych
echo "Zapisano!"ani ostrzeżeń PHP w odpowiedzi – jeden śmieciowy znak iresponse.json()rzuciSyntaxError: Unexpected token, - pliki muszą leżeć w
htdocsi być otwierane przezhttp://localhost/…, nie dwuklikiem (file://nie uruchomi PHP).
Jeśli strona i PHP są w tym samym folderze htdocs, piszesz adres względny 'zapisz.php' i nie myślisz o CORS. Gdy front działa na Vite (localhost:5173), a PHP na XAMPP (localhost), to już dwa różne źródła – PHP musi dodać header('Access-Control-Allow-Origin: http://localhost:5173'), inaczej przeglądarka zablokuje odczyt odpowiedzi. Przy wysyłaniu JSON (Content-Type: application/json) przeglądarka najpierw pyta metodą OPTIONS (preflight) – PHP musi wtedy odesłać także header('Access-Control-Allow-Headers: Content-Type') i zakończyć skrypt bez dalszej pracy.
Pułapki
Ściąga
| Składnia | Co robi | Przykład |
|---|---|---|
fetch(url) | GET | await fetch('/zadania') |
fetch(url, { method, headers, body }) | inna metoda z ciałem | { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(obj) } |
res.ok / res.status | czy 2xx / konkretny kod | if (!res.ok) throw new Error(res.status) |
await res.json() | ciało → obiekt | const dane = await res.json() |
PATCH vs PUT | część vs cały obiekt | PATCH /zadania/1 {"wykonane":true} |
form.addEventListener('submit', fn) | wysyłka także Enterem, z walidacją HTML5 | e.preventDefault() w środku |
new FormData(form) | pary name→value | Object.fromEntries(new FormData(form)) |
body: new FormData(form) | jak klasyczny formularz ($_POST, pliki) | bez Content-Type! |
npx json-server db.json | REST API z pliku na :3000 | ./public serwowany automatycznie |
file_get_contents('php://input') | PHP: surowe ciało JSON | json_decode($surowe, true) |
header('Content-Type: application/json') | PHP: odpowiedź to JSON | przed pierwszym echo |
http_response_code(201) | PHP: ustaw status | 400 dla błędu danych |
Zadania
Tłumacz kodów statusu
Napisz funkcję opiszStatus(kod), która zwraca napis w formacie "<kod> – <opis>" dla kodów 200, 201, 204, 400, 401, 403, 404, 500. Dla innego kodu z zakresu 200–299 zwróć "<kod> – sukces", z zakresu 400–499 "<kod> – błąd klienta", 500–599 "<kod> – błąd serwera", a poza tym "<kod> – nieznany". Przetestuj na 10 wartościach, w tym 299, 418, 503, 42.
Pokaż rozwiązanie
const OPISY = {
200: 'OK',
201: 'utworzono',
204: 'brak treści',
400: 'złe żądanie',
401: 'wymagane logowanie',
403: 'brak uprawnień',
404: 'nie znaleziono',
500: 'błąd serwera',
};
function opiszStatus(kod) {
// najpierw dokładne dopasowanie, potem kategoria po pierwszej cyfrze
if (OPISY[kod]) return `${kod} – ${OPISY[kod]}`;
if (kod >= 200 && kod <= 299) return `${kod} – sukces`;
if (kod >= 400 && kod <= 499) return `${kod} – błąd klienta`;
if (kod >= 500 && kod <= 599) return `${kod} – błąd serwera`;
return `${kod} – nieznany`;
}
for (const kod of [200, 201, 204, 299, 400, 404, 418, 500, 503, 42]) {
console.log(opiszStatus(kod));
}Zgłoszenie usterki bez przeładowania
Zbuduj formularz zgłoszenia usterki w pracowni: pole sala (liczba 1–30, required), opis (textarea, required, min 10 znaków), lista priorytet (niski/średni/wysoki). Po wysłaniu:
- strona nie może się przeładować,
- dane lecą
POSTjako JSON nahttps://jsonplaceholder.typicode.com/posts(pola:title="Sala <nr>",body= opis,userId= 1 dla niskiego, 2 dla średniego, 3 dla wysokiego), - w trakcie wysyłki przycisk jest zablokowany, a w
<p id="status">jest napisWysyłam…, - po sukcesie:
Zgłoszenie nr <id> przyjęte.i wyczyszczony formularz; po błędzie:Błąd: <treść>na czerwono.
Pokaż rozwiązanie
<form id="form-usterka">
<label>Sala <input name="sala" type="number" min="1" max="30" required></label>
<label>Opis <textarea name="opis" required minlength="10" rows="2"></textarea></label>
<label>Priorytet
<select name="priorytet">
<option value="niski">niski</option>
<option value="średni" selected>średni</option>
<option value="wysoki">wysoki</option>
</select>
</label>
<button type="submit">Zgłoś</button>
</form>
<p id="status"></p>label { display: block; margin-bottom: 6px; }
input, textarea, select { display: block; width: 100%; max-width: 320px; }
.blad { color: #b3261e; }const form = document.getElementById('form-usterka');
const status = document.getElementById('status');
const PRIORYTET_NA_ID = { niski: 1, 'średni': 2, wysoki: 3 };
form.addEventListener('submit', async (e) => {
e.preventDefault();
const dane = Object.fromEntries(new FormData(form));
const przycisk = form.querySelector('button');
przycisk.disabled = true;
status.className = '';
status.textContent = 'Wysyłam…';
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify({
title: `Sala ${dane.sala}`,
body: dane.opis,
userId: PRIORYTET_NA_ID[dane.priorytet],
}),
});
if (!res.ok) throw new Error(`serwer odpowiedział ${res.status}`);
const zapisane = await res.json();
status.textContent = `Zgłoszenie nr ${zapisane.id} przyjęte.`;
form.reset();
} catch (err) {
status.className = 'blad';
status.textContent = 'Błąd: ' + err.message;
} finally {
przycisk.disabled = false;
}
});Lista zakupów na json-server
Stwórz folder zakupy, w nim db.json z zasobem produkty (pola: nazwa, ilosc, kupione) i trzema przykładowymi rekordami oraz folder public z index.html i app.js. Uruchom npx json-server db.json i zbuduj aplikację, która:
- po załadowaniu pobiera
GET /produktyi rysuje listę (nazwa × ilość, checkbox „kupione”, przycisk „+1”, przycisk „Usuń”), - formularz dodaje produkt (
POST), - checkbox zmienia pole
kupione(PATCH), - „+1” zwiększa
ilosco 1 (PATCHz nową wartością), - „Usuń” usuwa (
DELETE), - na dole wyświetla
Kupione: X z Y.
Sprawdź po każdej operacji, co zmieniło się w db.json.
Pokaż rozwiązanie
{
"produkty": [
{ "id": "1", "nazwa": "Chleb", "ilosc": 1, "kupione": false },
{ "id": "2", "nazwa": "Mleko", "ilosc": 2, "kupione": true },
{ "id": "3", "nazwa": "Jabłka", "ilosc": 6, "kupione": false }
]
}<!doctype html>
<html lang="pl">
<head><meta charset="utf-8"><title>Zakupy</title></head>
<body>
<h1>Lista zakupów</h1>
<form id="form-produkt">
<input name="nazwa" placeholder="Produkt" required>
<input name="ilosc" type="number" min="1" value="1" required>
<button>Dodaj</button>
</form>
<ul id="lista"></ul>
<p id="podsumowanie"></p>
<p id="status"></p>
<script src="app.js" defer></script>
</body>
</html>const lista = document.getElementById('lista');
const podsumowanie = document.getElementById('podsumowanie');
const status = document.getElementById('status');
const form = document.getElementById('form-produkt');
let produkty = []; // lokalna kopia stanu – żeby „+1” znało aktualną ilość
async function zapytanie(sciezka, metoda = 'GET', dane) {
const opcje = { method: metoda, headers: {} };
if (dane !== undefined) {
opcje.headers['Content-Type'] = 'application/json';
opcje.body = JSON.stringify(dane);
}
const res = await fetch(sciezka, opcje);
if (!res.ok) throw new Error(`${metoda} ${sciezka}: ${res.status}`);
return res.status === 204 ? null : res.json();
}
function rysuj() {
lista.innerHTML = '';
for (const p of produkty) {
const li = document.createElement('li');
li.innerHTML = `
<label>
<input type="checkbox" data-akcja="kupione" data-id="${p.id}" ${p.kupione ? 'checked' : ''}>
${p.nazwa} × ${p.ilosc}
</label>
<button data-akcja="plus" data-id="${p.id}">+1</button>
<button data-akcja="usun" data-id="${p.id}">Usuń</button>`;
lista.append(li);
}
const kupione = produkty.filter((p) => p.kupione).length;
podsumowanie.textContent = `Kupione: ${kupione} z ${produkty.length}`;
}
async function odswiez() {
try {
produkty = await zapytanie('/produkty');
rysuj();
status.textContent = '';
} catch (e) {
status.textContent = 'Błąd: ' + e.message;
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const dane = Object.fromEntries(new FormData(form));
try {
await zapytanie('/produkty', 'POST', {
nazwa: dane.nazwa,
ilosc: Number(dane.ilosc), // FormData daje napisy!
kupione: false,
});
form.reset();
odswiez();
} catch (err) {
status.textContent = 'Błąd: ' + err.message;
}
});
// jeden nasłuchiwacz na listę: zmiana checkboxa
lista.addEventListener('change', async (e) => {
if (e.target.dataset.akcja !== 'kupione') return;
try {
await zapytanie(`/produkty/${e.target.dataset.id}`, 'PATCH', { kupione: e.target.checked });
odswiez();
} catch (err) {
status.textContent = 'Błąd: ' + err.message;
e.target.checked = !e.target.checked; // cofnij, skoro serwer nie zapisał
}
});
// jeden nasłuchiwacz na listę: przyciski
lista.addEventListener('click', async (e) => {
const { akcja, id } = e.target.dataset;
if (!akcja || akcja === 'kupione') return;
try {
if (akcja === 'plus') {
const produkt = produkty.find((p) => p.id === id);
await zapytanie(`/produkty/${id}`, 'PATCH', { ilosc: produkt.ilosc + 1 });
} else if (akcja === 'usun') {
await zapytanie(`/produkty/${id}`, 'DELETE');
}
odswiez();
} catch (err) {
status.textContent = 'Błąd: ' + err.message;
}
});
odswiez();Licznik głosów w PHP + fetch
W htdocs/glosowanie/ zbuduj ankietę „Który język wolisz?” z trzema przyciskami (JavaScript, PHP, Python) i wynikami pod spodem. Po stronie serwera napisz glosy.php, który:
- przy
GETodsyła JSON{"JavaScript": 3, "PHP": 1, "Python": 2}(dane trzymaj w plikuglosy.jsonobok skryptu, czytajfile_get_contents, zapisujfile_put_contents), - przy
POSTz ciałem JSON{"jezyk": "PHP"}zwiększa licznik i odsyła zaktualizowany obiekt ze statusem200, - dla nieznanego języka odsyła
400z{"blad": "Nieznany język"}.
Po stronie klienta: po załadowaniu GET, po kliknięciu POST, wyniki rysowane jako lista z procentami (zaokrąglonymi do całości), przyciski zablokowane podczas żądania, komunikat błędu z blad z odpowiedzi serwera.
Pokaż rozwiązanie
<?php
header('Content-Type: application/json; charset=utf-8');
$plik = __DIR__ . '/glosy.json';
$dozwolone = ['JavaScript', 'PHP', 'Python'];
// wczytaj stan (albo zera, gdy pliku jeszcze nie ma)
$glosy = file_exists($plik) ? json_decode(file_get_contents($plik), true) : [];
foreach ($dozwolone as $j) {
$glosy[$j] = $glosy[$j] ?? 0;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$dane = json_decode(file_get_contents('php://input'), true);
$jezyk = $dane['jezyk'] ?? '';
if (!in_array($jezyk, $dozwolone, true)) {
http_response_code(400);
echo json_encode(['blad' => 'Nieznany język'], JSON_UNESCAPED_UNICODE);
exit;
}
$glosy[$jezyk]++;
file_put_contents($plik, json_encode($glosy));
}
// GET oraz udany POST kończą się tak samo: aktualny stan
echo json_encode($glosy);<!doctype html>
<html lang="pl">
<head><meta charset="utf-8"><title>Głosowanie</title></head>
<body>
<h1>Który język wolisz?</h1>
<p id="przyciski">
<button data-jezyk="JavaScript">JavaScript</button>
<button data-jezyk="PHP">PHP</button>
<button data-jezyk="Python">Python</button>
</p>
<ul id="wyniki"></ul>
<p id="status"></p>
<script src="app.js" defer></script>
</body>
</html>const przyciski = document.getElementById('przyciski');
const wyniki = document.getElementById('wyniki');
const status = document.getElementById('status');
function rysuj(glosy) {
const suma = Object.values(glosy).reduce((a, b) => a + b, 0);
wyniki.innerHTML = '';
for (const [jezyk, liczba] of Object.entries(glosy)) {
const procent = suma === 0 ? 0 : Math.round((liczba / suma) * 100);
const li = document.createElement('li');
li.textContent = `${jezyk}: ${liczba} (${procent}%)`;
wyniki.append(li);
}
}
async function zapytanie(opcje) {
const res = await fetch('glosy.php', opcje);
const dane = await res.json(); // PHP zawsze odsyła JSON – także przy 400
if (!res.ok) throw new Error(dane.blad ?? `status ${res.status}`);
return dane;
}
function blokuj(stan) {
for (const b of przyciski.querySelectorAll('button')) b.disabled = stan;
}
przyciski.addEventListener('click', async (e) => {
const jezyk = e.target.dataset.jezyk;
if (!jezyk) return;
blokuj(true);
status.textContent = 'Zapisuję głos…';
try {
rysuj(await zapytanie({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jezyk }),
}));
status.textContent = `Dziękujemy za głos na ${jezyk}.`;
} catch (err) {
status.textContent = 'Błąd: ' + err.message;
} finally {
blokuj(false);
}
});
// start: pobierz aktualne wyniki
zapytanie().then(rysuj).catch((err) => (status.textContent = 'Błąd: ' + err.message));Uwaga do testów: zmień w app.js jezyk na 'Rust' (albo wyślij z konsoli fetch('glosy.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"jezyk":"Rust"}' })) i sprawdź, że dostajesz 400 z komunikatem Nieznany język.
Test powtórkowy
Test powtórkowy
Dziewięć pytań o REST, fetch i formularze. Jedna poprawna odpowiedź.
Które żądanie w stylu REST tworzy nowe zadanie?
Serwer odpowiedział statusem 201. Co to znaczy?
Co zwróci res.ok, gdy serwer odpowie statusem 500?
Który fragment poprawnie wysyła obiekt dane jako JSON?
Które zdarzenie i metoda zapobiegają przeładowaniu strony po wysłaniu formularza?
Co zawiera zmienna dane?
// <form><input name="imie" value="Ola"><input name="wiek" value="17"></form>
const dane = Object.fromEntries(new FormData(form));Wysyłasz plik przez body: new FormData(form). Który nagłówek Content-Type ustawiasz ręcznie?
Jak skrypt PHP odczytuje ciało żądania wysłane jako JSON?
Które polecenie uruchamia lokalne REST API z pliku db.json w aktualnej wersji json-server?