Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Masz dość walki z kodem i samym sobą? 🔄 Czas na RESET! ✅ Dołącz do bezpłatnego wyzwania!
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ł:
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! 🎯