Skip to content

Warstwa API ​

Cała komunikacja z backendem przechodzi przez jeden singleton api, będący instancją klasy Http zdefiniowanej w src/modules/core/services/http.js.

js
import { api } from "modules/core/services/http";

const { data } = await api.get("/blog/articles", { params: { page: 1 } });

Klasa udostępnia metody get(url, config), post(url, data, config), patch(url, data, config), put(url, data, config) oraz delete(url, data). Surowa instancja axiosa dostępna jest pod właściwością api.instance.

Konfiguracja ​

js
// src/modules/core/services/http.js
this.instance = axios.create({
  baseURL: process.env.API_BASE_URL,
  withCredentials: true,
  withXSRFToken: true,
});

this.setAuthorizationToken();   // Bearer z LocalStorage("token")
this.listenToResponse();        // interceptor odpowiedzi
this.setLocaleInterceptor();    // interceptor żądania

Wartość API_BASE_URL pochodzi z pliku .env, co opisują Pierwsze kroki.

Interceptor żądania — język ​

Do każdego żądania dopinane są dwa parametry:

js
// src/modules/core/services/http.js
config.params.lang = config.params.lang || this.locale;
config.params.content_lang = config.params.content_lang || this.contentLocale;

lang określa język interfejsu, content_lang — język tłumaczonej treści. Ustawiają je metody api.setLocale() i api.setContentLocale(), wywoływane z pliku boot i18n oraz ze store'a core/locale.

Języka nie dodaje się do kluczy Vue Query

Skoro parametry lang i content_lang przesyłane są automatycznie, nie ma potrzeby umieszczania ich w queryKey. Po zmianie języka plik App.vue unieważnia cały cache, co opisuje Vue Query. Język pojedynczego żądania można nadpisać, przekazując params.lang jawnie.

Interceptor odpowiedzi — toasty i błędy ​

Obsługa odpowiedzi jest scentralizowana w pliku src/modules/core/helpers/response.js. Komponenty nie powinny wystawiać własnych toastów dla błędów API.

Odpowiedzi 2xx — handle200Status ​

Jeżeli backend zwróci pole message, wyświetlany jest toast według poniższych reguł:

Warunek w odpowiedziEfekt
data.message przy data.status === "fail"toast negatywny
data.message w pozostałych przypadkachtoast pozytywny
data.show_message === falsetoast wyciszony

Wyciszanie toasta

Gdy endpoint zwraca pole message, a toast jest niepożądany — na przykład przy pobieraniu listy — backend powinien dołączyć show_message: false.

Błędy — handleErrors ​

StatusZachowanie
422handleValidationErrors — osobny toast dla każdego komunikatu każdego pola, z timeoutem 10 sekund i przyciskiem zamknięcia
401, 403handleUnauthorized — toast z zawartością data.message
pozostałetoast negatywny z data.message albo tekstem "Error"

Dodatkowo przy statusie 401, poza żądaniem auth/logout, interceptor wywołuje authStore.logout(), co opisuje Autentykacja.

Błędy 422 w formularzach

Interceptor wyświetla toasty, natomiast za przypisanie błędów do konkretnych pól formularza odpowiada useFormSubmit. Statusu 422 nie należy obsługiwać ręcznie w komponencie, co szerzej opisują Formularze.

Wyłączenie interceptora ​

js
api.noListenToResponse();

Metoda odpina interceptor odpowiedzi. Przydaje się, gdy dla wybranego przepływu obsługa błędów ma zostać zaimplementowana samodzielnie — od tego momentu żaden toast nie pojawi się automatycznie.

Warstwa endpointów ​

Pliki w katalogu api/endpoints/ są celowo pozbawione logiki, obsługi błędów i transformacji danych:

js
// src/modules/blog/api/endpoints/article.js
import { api } from "modules/core/services/http";

const url = "/blog/articles";

export const getArticles = (params) => api.get(url, { params });
export const getArticle = (id) => api.get(`${url}/${id}`);
export const createArticle = (data) => api.post(url, data);
export const updateArticle = (id, data) => api.patch(`${url}/${id}`, data);
export const deleteArticle = (id) => api.delete(`${url}/${id}`);

Stała url pełni funkcję identyfikatora zasobu

Ta sama ścieżka trafia do createEntityKeys("/blog/articles") w pliku api/keys.js oraz jako resource do useCoreTable. Dzięki temu klucze cache i endpointy nie mogą się rozjechać.

Transformacje danych oraz obsługa cache należą do warstwy wyżej, czyli do katalogów api/queries/ i api/mutations/, opisanych w sekcji Vue Query.

Kontrakty odpowiedzi ​

Lista paginowana ​

json
{
  "results": [ { "id": 1, "title": "..." } ],
  "pagination": {
    "total": 128,
    "current_page": 1,
    "last_page": 13
  }
}

Kształtu tego wymagają dwa composables: useCoreTable korzysta z results i pagination.total, a useCoreInfiniteQuery — z pagination.current_page oraz pagination.last_page.

Pojedynczy rekord ​

json
{ "model": { "id": 1, "title": "..." } }

Z tego powodu zapytania po szczegóły odczytują pole data.model:

js
// src/modules/blog/api/queries/article.queries.js
const { data } = await getArticle(toValue(id));
return data.model;

Błąd walidacji 422 ​

json
{
  "message": "The given data was invalid.",
  "errors": {
    "title": ["Pole tytuł jest wymagane."],
    "items.0.name": ["Pole nazwa jest wymagane."]
  }
}

Klucze zapisane w notacji kropkowej, takie jak items.0.name, zamieniane są na notację stosowaną przez vee-validate, czyli items[0].name. Szczegóły opisują Formularze.

Wysyłanie plików ​

Do zbudowania obiektu FormData z zagnieżdżonej struktury służy funkcja pomocnicza:

js
import { objectToFormData } from "modules/core/helpers/object-to-formdata";

const formData = objectToFormData(values, { booleansAsIntegers: true });
await api.post("/some/endpoint", formData);

Obsługuje zagnieżdżone obiekty i tablice, pliki, a także opcje booleansAsIntegers oraz allowEmptyArrays.

Biblioteka mediów ma własny przepływ uploadu

Do wgrywania plików do biblioteki nie buduje się FormData ręcznie — gotowy przepływ opisuje sekcja Media.

Made by Moonbite