Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!

🔥 Webinar Q&A na temat IT – pytaj, o co chcesz! już 24.11.2022 (czwartek) o 20:30

CRUD w osobnym pliku – lepszy kod dla API

Jak zachować porządek w kodzie

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.

Spis treści

 Co to jest CRUD

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 realizujemy je z użyciem metod HTTP:

  • POST dla tworzenia
  • GET dla odczytywania
  • PUT lub PATCH dla aktualizacji
  • DELETE do usuwania.

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.

 CRUD w osobnym pliku

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.

 CRUD – klasa w 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.

 Kod JavaScript przed refaktoryzacją

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.

 Stworzenie klasy dla CRUD w JS

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.

 CRUD – eksport pojedynczych funkcji

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.

Uwaga! Jeżeli jeszcze nie znasz hooków, to resztę artykułu (kontynuowaną od tego momentu) opartą o przykłady komponentów klasowych znajdziesz w repozytorium crud-functions-export-react-classes.

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;

 CRUD – wykorzystanie w aplikacji

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.

 Klasa czy eksport funkcji

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.

 JSON Server – przetestuj CRUD

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:

  1. Zmienię imię pierwszego kota:
    update('/kittens', 1, { name: 'Gigi' });
  2. Usunę drugiego kota z bazy:
    remove('/kittens', 2);
  3. Dodam nowego kota:
    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ł:

Mentoring to efektywna nauka pod okiem doświadczonej osoby, która:

  • przekazuje Ci swoją wiedzę i nadzoruje Twoje postępy w zdobywaniu umiejętności,
  • uczy Cię dobrych praktyk i wyłapuje złe nawyki,
  • wspiera Twój rozwój i zwiększa zaangażowanie w naukę.