Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Potrzebujesz wsparcia? Oceny CV? A może Code Review? ✅ Dołącz do Discorda!
Twórz lepiej zorganizowany, czytelny i reużywalny kod w pracy z API. W tym artykule znajdziesz dwa sposoby uporządkowania kodu do obsługi API w osobnych plikach na przykładzie klasy oraz eksportu pojedynczych funkcji. Zostało to omówione na przykładzie czystego JavaScriptu i Reacta.
Chcesz być (lepszym) programistą i lepiej zarabiać? Umów się na rozmowę - powiem Ci jak to zrobić!
CRUD to akronim od słów create, read, update, delete, czyli twórz, odczytuj, aktualizuj, usuwaj. Słowa te określają operacje na danych. W JavaScripcie (frontend i backend) realizujemy je z użyciem metod HTTP przy komunikacji w API:
Prościej mówiąc: to metody, których potrzebujesz, by wysyłać, odbierać i modyfikować dane na serwerze za pomocą API. Jeśli uczyłeś się już o asynchroniczności w JavaScripcie i pierwsze zapytania do API (lub pracę z JSON Serverem) masz za sobą, to dzięki temu artykułowi nauczysz się pisać lepszy kod i porządkować go w osobnych plikach.
Dlaczego w ogóle warto przenieść logikę odpytywania API do osobnego pliku? Po pierwsze: dzięki temu Twój kod jest bardziej czytelny – im mniej kodu w poszczególnych plikach, tym lepiej. Po drugie: w ten sposób możesz korzystać z raz zdefiniowanych metod w różnych miejscach swojej aplikacji, przestrzegając tym samym zasady DRY (Don’t Repeat Yourself).
Jaką formę może przybierać kod z CRUD-em w osobnym pliku? Może to być na przykład klasa lub zbiór oddzielnie eksportowanych funkcji. Teraz omówimy oba te przypadki – pierwszy na podstawie JavaScriptu, drugi: Reacta. Możesz oczywiście stosować je zamiennie – klasy w Reakcie i eksport funkcji w czystym JavaScripcie.
Uwaga: na potrzeby tego artykułu korzystam z API o kotach, które – jako ogólnodostępne – umożliwia jedynie odczyt danych (metoda GET). Nic nie stoi jednak na przeszkodzie, abyś przetestował resztę metod z JSON Serverem, o którym piszę na końcu.
Stwórzmy prosty program, który będzie pobierał randomową ciekawostkę o kotach i wyświetlał ją użytkownikowi. Jeśli mielibyśmy dostęp do modyfikacji i usuwania danych w API i chcieli stworzyć wszystkie potrzebne metody „na przyszłość”, to kod przed refaktoryzacją (przed przeniesieniem CRUD-a do osobnego pliku) tworzyłby długi ciąg wersów:
document.addEventListener('DOMContentLoaded', function () { const factEl = document.querySelector('.fact'); const url = 'https://catfact.ninja'; function get(resource) { const path = url + resource; const promise = fetch(path); return promise .then(resp => { if (resp.ok) { return resp.json(); } return Promise.reject(resp); }) .catch(err => console.error(err)) .finally(() => { console.log('Odpytywanie API zakończone!'); }); } function create(resource, data) { const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, }; const path = url + resource; const promise = fetch(path, options); return promise .then(resp => { if (resp.ok) { return resp.json(); } return Promise.reject(resp); }) .catch(err => console.error(err)) .finally(() => { console.log('Odpytywanie API zakończone!'); }); } // function remove(resource, id) { ... } // function update(resource, id, updatedEl) { ... } get('/fact').then(function (response) { factEl.innerText = response.fact; }); });
Utrudnia nam to zrozumienie, za co odpowiada nasz plik. Tak naprawdę chcemy tylko pobrać koci fakt i go wyświetlić:
get('/fact').then(function (response) { factEl.innerText = response.fact; });
Jeżeli logikę odpowiadającą np. za dodawanie nowych danych do bazy przeniesiemy do osobnych plików (np. osobno dodawanie kocich ras i osobno faktów), to nadal nie będzie to optymalne: w każdym pliku bowiem na nowo będziemy musieli tworzyć funkcję create() z odpowiednimi parametrami. Nie jest to zgodne z DRY.
Stwórzmy nowy plik CatFactsProvider.js. To w nim umieścimy wszystkie potrzebne metody. W konstruktorze określamy adres URL, pod którym mieszczą się zasoby. Ponieważ metoda fetch() prezentuje się wszędzie tak samo (różni się tylko opcjami), możemy wyodrębnić ją do osobnej funkcji, a opcje przekazywać przez parametr. Dzięki temu metody w klasie są czytelniejsze.
class CatFactsProvider { constructor() { this.url = 'https://catfact.ninja'; } get(resource) { return this._fetch(resource); } create(resource, data) { const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, }; return this._fetch(resource, options); } remove(resource, id) { const options = { method: 'DELETE', }; return this._fetch(resource, options, id); } update(resource, id, updatedEl) { const options = { method: 'PATCH', body: JSON.stringify(updatedEl), headers: { 'Content-Type': 'application/json' }, }; return this._fetch(resource, options, id); } _fetch(resource = '', options = { method: 'GET' }, id = '') { const path = this.url + resource + `/${id}`; const promise = fetch(path, options); return promise .then(resp => { if (resp.ok) { return resp.json(); } return Promise.reject(resp); }) .catch(err => console.error(err)) .finally(() => { console.log('Odpytywanie API zakończone!'); }); } } export default CatFactsProvider;
Co dalej? Czas na import klasy w pliku, w którym jej potrzebujemy. U nas to app.js. Następnie tworzymy nową instancję klasy, aby móc skorzystać z odpowiednich metod. Potrzebujemy tylko opcję pobierania danych – a dokładniej kociego faktu – co przekazujmy w argumencie: api.get('/fact')
.
import CatFactsProvider from './CatFactsProvider.js'; document.addEventListener('DOMContentLoaded', function () { // pobieram element <p> ze strony const factEl = document.querySelector('.fact'); const api = new CatFactsProvider(); api.get('/fact').then(function (response) { // umieszczam treść ciekawostki w elemencie <p> factEl.innerText = response.fact; }); });
Zobacz, jak bardzo skrócił nam się kod! Dosłownie do kilku linijek. Całość kodu znajdziesz w repozytorium na GitHubie, a działanie podejrzysz dzięki GitHub Pages.
Uwaga: w create-react-app (od Reacta w wersji 18) w trybie deweloperskim – który po instalacji paczek uruchomisz komendą npm run start
– zauważysz, że kod z hooka useEffect (lub metody componentDidMount) wykonuje się dwa razy (dwukrotnie jest odpytywane API). To wynik działania strict mode, czyli tzw. trybu ścisłego, który chroni kod przed błędami i złymi praktykami. W tej chwili nie zwracaj na to uwagi – w trybie produkcyjnym kod wykona się raz.
Wróćmy do naszego zagadnienia.
Sposób, który teraz przedstawię, możemy wykorzystać zarówno w czystym JavaScripcie, jak i różnych bibliotekach i frameworkach. My zrobimy to na przykładzie Reacta. Kod dla tego rozwiązania znajdziesz na repozytorium na GitHubie. Tam też tłumaczę, w jaki sposób pobrać ten projekt, zainstalować u siebie zależności i uruchomić tryb developerski (jeśli nie znasz hooków, to kod dla komponentów klasowych Reacta znajdziesz w białej ramce poniżej, na razie prześledź kod dla catFactsPrividera).
Zamiast tworzyć klasę, możemy w podobny sposób napisać osobne funkcje i eksportować pojedyncze z nich (pomijamy więc eksport fetchData(), ponieważ tę funkcję wykorzystujemy tylko w pliku catFactsProvider.js). Taki provider może więc wyglądać w następujący sposób:
const url = 'https://catfact.ninja'; export function get(resource) { return fetchData(resource); } export function create(resource, data) { const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, }; return fetchData(resource, options); } export function remove(resource, id) { const options = { method: 'DELETE', }; return fetchData(resource, options, id); } export function update(resource, id, updatedEl) { const options = { method: 'PATCH', body: JSON.stringify(updatedEl), headers: { 'Content-Type': 'application/json' }, }; return fetchData(resource, options, id); } function fetchData(resource = '', options = { method: 'GET' }, id = '') { const path = url + resource + `/${id}`; const promise = fetch(path, options); return promise .then(resp => { if (resp.ok) { return resp.json(); } return Promise.reject(resp); }) .catch(err => console.error(err)) .finally(() => { console.log('Odpytywanie API zakończone!'); }); }
Jak widzisz, są to zupełnie normalne funkcje – trzeba tylko pamiętać o słowie kluczowym export.
Teraz znów chcemy jedynie wyświetlić użytkownikowi randomową ciekawostkę, potrzebujemy więc funkcji get() w pliku App.js. Wystarczy, że zaimportujemy ją z pliku catFactsProvider.js:
import { get } from './catFactsProvider.js';
Następnie pobieramy dane z API w momencie renderowania komponentu App – używamy więc do tego hooka useEffect – i umieszczamy pobraną informację w stanie komponentu (hook useState). Treść wyświetlamy użytkownikowi w elemencie <p>.
import { useState, useEffect } from 'react'; import './App.css'; import { get } from './catFactsProvider.js'; function App() { const [catFact, setCatFact] = useState(''); useEffect(function () { get('/fact').then(function (resp) { setCatFact(resp.fact); }); }, []); return ( <div className="App"> <header className="App-header"> <p>{catFact}</p> </header> </div> ); } export default App;
Teraz pokażę Ci zaletę trzymania metod dla API w osobnym pliku. Ten przykład także oprę na Reakcie, lecz oczywiście podejście to możemy wykorzystać również w czystym JS-ie czy innych bibliotekach/frameworkach.
Załóżmy, że prócz faktu o kotach, chcemy wyświetlić też jakąś rasę. Zamiast tworzyć tę logikę w pliku App.js i go „zaśmiecać”, możemy stworzyć sobie customowy hook useRandomCat.js (w JS-ie byłby to osobny plik z odpowiednim kodem i eksportem) i w nim odpytać API, korzystając z gotowych metod z pliku catFactsProvider.js.
Zauważ, że korzystamy z możliwości odpytania innego zasobu. Wcześniej był to '/fact'
, a teraz są rasy: '/breeds'
.
import { useState, useEffect } from 'react'; import { get } from './catFactsProvider'; const useRandomCat = () => { const [cat, setCat] = useState({}); useEffect(function () { get('/breeds').then(resp => { const random = resp.data[Math.floor(Math.random() * resp.data.length)]; setCat(random); }); }, []); return cat; }; export default useRandomCat;
To, co zwracamy, to randomowy obiekt z informacjami o rasie: nazwie, pochodzeniu, długości sierści itp. My wykorzystamy tylko nazwę i wyświetlimy ją pod ciekawostką:
import { useState, useEffect } from 'react'; import './App.css'; import { get } from './catFactsProvider.js'; import useRandomCat from './useRandomCat'; function App() { const [catFact, setCatFact] = useState(''); const cat = useRandomCat(); useEffect(function () { get('/fact').then(function (resp) { setCatFact(resp.fact); }); }, []); return ( <div className="App"> <header className="App-header"> <p>{catFact}</p> <p>– {cat.breed} –</p> </header> </div> ); } export default App;
Zobacz, w jak niewielkim stopniu zmienił się plik App.js – doszły zaledwie trzy (podświetlone) linijki! Cała logika trafiła do customowego hooka, a dzięki temu, że trzymamy CRUD w osobnym pliku, mogliśmy z pomocą importu łatwo i szybko skorzystać z potrzebnej nam metody.
Tak naprawdę będzie to zależeć od Twoich decyzji podjętych na początku planowania kodu lub od projektu, do którego dołączysz w pracy. Ważne jest to, by nie mieszać różnych rozwiązań, jeśli nie ma ku temu przesłanek. Jeżeli masz wybór, to zastosuj to, z czym łatwiej Ci się pracuje, i trzymaj się tego w obrębie jednego projektu.
Tak jak mówiłem na początku, ogólnodostępne API umożliwiają jedynie odczytywanie danych. Możesz jednak przetestować nasz osobny plik z CRUD-em dzięki JSON Serverowi.
To rozwiązanie, które imituje działanie API – możesz więc odczytywać, dodawać, modyfikować i usuwać dane z fake’owego serwera (czyli pliku JSON na Twoim komputerze). Instalacja i uruchomienie są bardzo proste, więc jeśli jeszcze nie korzystałeś z tego narzędzia, zacznij koniecznie! 🙂
Ja stworzyłem sobie „bazę danych” z kociętami w pliku data.json w projekcie z Reactem (pamiętaj, że taki plik dla JSON Servera powinien się znajdować w katalogu głównym projektu).
{ "kittens": [ { "id": 1, "name": "Gloria", "age": "2 months" }, { "id": 2, "name": "Gutek", "lastName": "3 months" }, { "id": 3, "name": "Teofil", "lastName": "6 months" } ] }
Następnie zmieniłem adres URL w pliku catFactsProvider.js na nowe API, które zapewnia mi JSON Server:
const url = 'http://localhost:3000'
A teraz wykonam następujące operacje:
update('/kittens', 1, { name: 'Gigi' });
remove('/kittens', 2);
create('/kittens', { name: 'Max', age: '3 months' });
Działania te będą asynchroniczne, niezależnie od siebie, dlatego w kodzie nie zastosowałem async...await lub .then(), by poczekać na wynik poprzedniej operacji.
Uwaga: w prawdziwym projekcie operacji na bazie danych raczej nie wykonujemy przy uruchomieniu komponentu (tu: wewnątrz useEffect). Zrobiłem tak, by uprościć nasz przykład – chcemy jedynie sprawdzić, czy nastąpią zmiany w pliku data.json.
Uwaga: jak pisałem wyżej – jeżeli korzystamy z create-react-app, to w trybie developerskim powinniśmy spodziewać się dwukrotnego uruchomienia kodu wewnątrz useEffect. Aby nam to nie przeszkadzało, wyłączymy strict mode w pliku index.js:
const root = ReactDOM.createRoot(document.getElementById('root')); root.render( // <React.StrictMode> <App /> // </React.StrictMode> );
Ponieważ jednak strict mode jest po to, byśmy stosowali dobre praktyki, podrzucam Ci lepsze rozwiązanie do obsługi API. Polecam zapoznać się w wolnej chwili.
Całość prezentuje się w następujący sposób (możesz przekleić poniższy kod do pliku App.js):
import { useEffect } from 'react'; import './App.css'; import { update, remove, create } from './catFactsProvider.js'; function App() { useEffect(function () { update('/kittens', 1, { name: 'Gigi' }); remove('/kittens', 2); create('/kittens', { name: 'Max', age: '3 months' }); }, []); return ( <div className="App"> <header className="App-header"> <p>testujesz JSON Server - sprawdź zmiany w pliku data.json</p> </header> </div> ); } export default App;
Po uruchomieniu kodu zmieniła się nasza baza danych zgodnie z tym, jakich metod użyłem: Gloria ma teraz na imię Gigi, obiekt z Gutkiem zniknął z bazy i pojawił się nowy obiekt z Maxem.
{ "kittens": [ { "id": 1, "name": "Gigi", "age": "2 months" }, { "id": 3, "name": "Teofil", "lastName": "6 months" }, { "name": "Max", "age": "3 months", "id": 4 } ] }
Jeżeli chcesz, możesz nasz program rozbudować o dodatkowe opcje, np. formularz dodawania kociąt. Dzięki temu wykorzystanie CRUD-a stanie się bardziej naturalne – raczej bowiem przy pierwszym renderze komponentu nie korzystamy z innych metod niż get(). Jak już wspomniałem: zrobiłem to tylko na potrzeby zaprezentowania działania reszty metod 🙂
W pracy programisty takie porządki w kodzie czekają Cię w każdym projekcie (a przynajmniej w tym, który będzie utrzymywany i rozwijany). Dążymy bowiem do tego, by w razie potrzeby każdy w zespole zorientował się w stworzonych przez nas funkcjonalnościach (czytelność), mógł korzystać z reużywalnych modułów (CRUD w osobnym pliku) i nie tworzył niepotrzebnego kodu (zasada DRY).
Udostępnij ten artykuł:
Potrzebujesz cotygodniowej dawki motywacji?
Zapisz się i zgarnij za darmo e-book o wartości 39 zł!
PS. Zazwyczaj rozsyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu email.
Chcesz zostać (lepszym) programistą i lepiej zarabiać?
🚀 Porozmawiajmy o nauce programowania, poszukiwaniu pracy, o rozwoju kariery lub przyszłości branży IT!
Umów się na ✅ bezpłatną i niezobowiązującą rozmowę ze mną.
Chętnie porozmawiam o Twojej przyszłości i pomogę Ci osiągnąć Twoje cele! 🎯