<Quick.Webdev />

be in touch with web development

Komunikacja frontend-backend w React, Express i REST API - moje podejście.

Reading time: 6 minutes

W świecie Javascriptu istnieje mnóstwo frameworków i bibliotek do budowania aplikacji webowych. Najpopularniejszą technologią na frontendzie jest React, a po stronie serwera - Express, Koa czy Nest.js. Podczas budowania aplikacji fullstackowej ważne jest, aby zaplanować dobrze architekturę, strukturę katalogów i sposów przepływu danych w obrębie naszej aplikacji. W tym artykule przedstawię moje podejścia do problemu jakim jest komunikacja między tymi dwoma światami z użyciem tradycyjnego REST API.

Jako przykład, chciałbym przedstawić kilka części mojej aplikacji z quizami którą tworzyłem przez ostatnie miesiące. Kliknij, by przejść do projektu tutaj

Dashboard

W komponencie dashboard mamy listę z quizami stworzonymi przez użytkowników. Aby odczytać informacje o tych quizach musimy wykonać zapytanie do naszego API. Właśnie tutaj zaczyna się cała komunikacja.

Przejdź do => /src/pages/Dashboard/index.js

const Dashboard = () => {
  const { data: quizzes, loading, error } = useFetching(getQuizzes);

  if (loading) return <Loader />;
  if (error) return <ErrorPage msg={error} />;

  return (
    <Layout>
...

API na frontendzie

W linii drugiej wywołuje własny hook o nazwie useFetching z argumentem getQuizzes. Ten argument to referencja do funkcji w pliku /src/api.index.js. Zobaczmy jak ta funkcja wygląda a następnie pokażę co dzieje się w hooku useFetching.

export const getQuizzes = async () => {
  const { data } = await instance.get('/api/quizzes').catch(({ response }) => response);
  return data;
};

Jest to zwykła asynchroniczna funkcja, która wysyła zapytanie do ścieżki /api/quizzes używając paczki axios. Używam również metody catch aby wyłapać potencjalne błędy. Wszystkie informacje znajdują się we właściwości data, więc destrukturyzuje je używając składni ES6.

Hook useFetching

Przejdź do => /src/hooks/useFetching.js

export const useFetching = (method, ...methodArgs) => {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [data, setData] = useState(undefined);

  useEffect(() => {
    const cancelToken = axios.CancelToken;
    const source = cancelToken.source();

    (async () => {
      const { type, data, msg } = await method([...methodArgs]);

      if (type === responseTypes.success) {
        setData(data);
      } else {
        setError(msg);
      }

      setLoading(false);
    })();

    return () => source.cancel();
  }, []);

  return { data, loading, error };
};

useFetching to nic innego jak funkcja, która jako parametr przyjmuje referencję do funkcji, którą ma wywołać oraz tablicę opcjonalnych parametrów, które ta funkcja przyjmuje. Poniżej deklaruję stan (loading, error i data). W linii 10 wywołuję przekazaną jako parametr funkcję z argumentami używając spread operatora i wtedy desktrukturyzuję response, data i wiadomość z błędem (tutaj to wartość o nazwie msg). Potem, jeżeli zapytanie zakończyło się pomyślnie ustawiam zmienną data, error i wartość zmiennej loading na false aby dać znak, że żądanie się zakończyło, a komponent loader może zniknąć. Pod koniec zwracam obiekt z niezbędnymi danymi do wyświetlenia w komponencie. Nie zapominaj by zakończyć wszyskie aktywne subskrypcje i zapytania asynchroniczne w funkcji czyszczącej komponent (cleanup function). Ja to robię używając accessTokena, który daje nam paczka axios. Teraz, wróćmy do naszego komponentu Dashboard.

  const { data: quizzes, loading, error } = useFetching(getQuizzes);

Tutaj destrukturyzuję zwrócony obiekt z hooka, a następnie używam tych danych do zwizualizowania naszego stanu w UI.

  if (loading) return <Loader />;
  if (error) return <ErrorPage msg={error} />;

I na końcu iterujemę po tablicy z quizami.

Backend w Express

Przejdźmy teraz do backendu, aby zobaczyć w jaki sposób nasze zapytanie jest obsługiwane.

Backend tutaj, zapraszam

Przejdź do => /routes/api.js

router.get("/quizzes", verify, quizzesController.getQuizzes);

W tej linii tworzę ścieżkę GET /quizzes która obsługuje zapytanie. Teraz zapytanie jest przekazywane do dedykowanego kontrolera.

Kontroler

Rolą kontrolerów jest:

  • Przyjąć zapytanie
  • Wywołać do pracy konkretny Service (jeżeli to konieczne)
  • Zwrócić dane (lub błąd)

Właśnie to dzieję się w pliku /controllers/quizzes.js.

module.exports.quizzesController = {
  getQuizzes: async (req, res) => {
    if (req.user) {
      const { type, data, msg } = await QuizzesService.getQuizzes();

      if (type === responseTypes.success) {
        res.status(200).json({ type, data });
      } else {
        res.status(401).json({ type, msg });
      }
    }
  },
...

Tutaj mamy metodę getQuizzes, która spełnia wyżej wymienione reguły:

  • Przyjmuje zapytanie (linia 2)
  • Wywołuje odpowiedni Service (linia 4)
  • Zwraca dane lub błąd w zależności od pola type (linie 6-10)
Pierwsza i trzecia linia to kroki raczej trywialne. Najciekawsza rzecz dzieje się w linii 4, gdzie wywołujemy do pracy Service.

Service

Rolą serwisów jest obsługa bardziej zaawansowanych i czasochłonnych zadań takich jak wykonywanie zapytań do bazy danych, wykonywanie obliczeń, czy przygotowywanie gotowego obiektu do odesłania kontrolerowi.

class QuizzesService {
  async getQuizzes() {
    try {
      const quizzes = await Quiz.find({}).sort({ creationDate: -1 });

      if (quizzes.length > 0) {
        return {
          type: responseTypes.success,
          data: quizzes,
        };
      } else throw messages.QUIZZES_NOT_FOUND;
    } catch (e) {
      return makeResponse(e, responseTypes.error);
    }
  }
...

W ostatnim kroku, wykonuję zapytanie do MongoDB. Dostaję wszystkie quizy, i sortuję je według daty publikacji. Jeżeli quizy istnieją, zwraca je - w przeciwnym przypadku wyłapuję błąd i odsyłam odpowiedź.

W tym miejscu polecam Ci używać składni try catch aby Twój kod był bardziej czytelny i łatwy do debugowania.

Voila! Nasza komunikacja właśnie dobiegła końca. Teraz, dane zwrócone z Service'u odsyłamy kontrolerowi, a następinie na część frontendową. Następnie rozwiązywana zostaje funkcja w api, hooku useFetching i stamtąd trafia do odpowiedniego Reacowego komponentu, gdzie zaczęła się cała długa, ale fajna podróż! 😎

Podsumowując, tak wygląda komunikacja krok po kroku:

Komponent Reactowy -> hook useFetching -> metoda w API -> ścieżka na backend -> kontroler -> Service (tutaj mamy nasze dane) -> kontroler -> metoda API -> hook useFetching -> komponent Reactowy (korzystamy z danych).

Other articles:

Krzysztof Szczepański - All rights reserved 2021