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

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 w trybie deweloperskim (który po instalacji paczek uruchomisz komendą npm run start) zauważysz, że komponent renderuje się dwukrotnie (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. Nie zwracaj na to uwagi – w trybie produkcyjnym render będzie wykonywać 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.

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;

 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.

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(). 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ę.