Motyw
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 żądaniaWartość 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 odpowiedzi | Efekt |
|---|---|
data.message przy data.status === "fail" | toast negatywny |
data.message w pozostałych przypadkach | toast pozytywny |
data.show_message === false | toast 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
| Status | Zachowanie |
|---|---|
422 | handleValidationErrors — osobny toast dla każdego komunikatu każdego pola, z timeoutem 10 sekund i przyciskiem zamknięcia |
401, 403 | handleUnauthorized — toast z zawartością data.message |
| pozostałe | toast 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.