# Best practices when querying the API from UI components If you need to query the API from a component, there are several ways to do so. This document will help you decide which one to use and provide some best practices when doing so to keep the code clean and maintainable. ## Choose the Right Approach When querying APIs in Vue components, consider the following approaches and their best practices: ## 1. Prefer Composables over Stores over Direct API Calls ### Composables - **What Are Composables?**: please read the official Vue documentation on composables [here](https://vuejs.org/guide/reusability/composables.html). If there is already a composable that takes care of the logic you need for accessing a particular resource in the API please use it or consider writing a new one. They provide a type-safe interface and a higher level of abstraction than the related Store or the API itself. They might rely on one or more Stores for caching and reactivity. ### Stores - **Stores Explained**: Please read the official Vue documentation on State Management [here](https://vuejs.org/guide/scaling-up/state-management.html). If there is no Composable for the API endpoint you are using, try using a (Pinia) Store instead. Stores are type-safe and provide a reactive interface to the API. They can also be used to cache data, ensuring a single source of truth. - **Use Pinia Stores**: If you need to create a new Store, make sure to create a Pinia Store, and not a Vuex Store as Pinia will be the replacement for Vuex. Also, use [Composition API syntax over the Options API](https://vuejs.org/guide/extras/composition-api-faq.html) for increased readability, code organization, type inference, etc. ### Direct API Calls - If the type of data you are querying should not be cached, or you just need to update or create new data, you can use the API directly. Make sure to use the **GalaxyApi client** (see below) instead of Axios, as it provides a type-safe interface to the API along with some extra benefits. ## 2. Prefer **GalaxyApi client** over Axios (when possible) - **Use **GalaxyApi client** with OpenAPI Specs**: If there is an OpenAPI spec for the API endpoint you are using (in other words, there is a FastAPI route defined in Galaxy), always use the GalaxyApi client. It will provide you with a type-safe interface to the API. **Do** ```ts import { ref, onMounted } from "vue"; import { GalaxyApi, type HDADetailed } from "@/api"; import { errorMessageAsString } from "@/utils/simple-error"; interface Props { datasetId: string; } const props = defineProps(); const datasetDetails = ref(); const errorMessage = ref(); async function loadDatasetDetails() { // Your IDE will provide you with autocompletion for the route and all the parameters const { data, error } = await GalaxyApi().GET("/api/datasets/{dataset_id}", { params: { path: { dataset_id: props.datasetId, }, query: { view: "detailed" }, }, }); if (error) { // Handle error here. For example, you can display a message to the user. errorMessage.value = errorMessageAsString(error); // Make sure to return here, otherwise `data` will be undefined return; } // Use `data` here. We are casting it to HDADetailed to help the type inference because // we requested the "detailed" view and this endpoint returns different types depending // on the view. In general, the correct type will be inferred automatically using the // API schema so you don't need to (and shouldn't) cast it. datasetDetails.value = data as HDADetailed; } onMounted(() => { loadDatasetDetails(); }); ``` **Don't** ```ts import { ref, onMounted } from "vue"; import axios from "axios"; import { getAppRoot } from "onload/loadConfig"; import { errorMessageAsString } from "@/utils/simple-error"; interface Props { datasetId: string; } const props = defineProps(); const datasetDetails = ref(); const errorMessage = ref(); async function loadDatasetDetails() { // You need to construct the URL yourself const url = `${getAppRoot()}api/datasets/${datasetId}`; // You are forced to use a try-catch block to handle errors // and you may forget to do so. try { // This is not type-safe and cannot detect changes in the API schema. const response = await axios.get(url, { // You need to know the API parameters, no type inference here. params: { view: "detailed" }, }); // In this case, you need to cast the response to the correct type // (as in the previous example), but you will also have to do it in the general // case because there is no type inference. datasetDetails = response.data as HDADetailed; } catch (e) { errorMessage.value = errorMessageAsString(error); } } onMounted(() => { loadDatasetDetails(); }); ``` > **Reason** > > The `GalaxyApi client` function provides a type-safe interface to the API, and is already configured to use the correct base URL. In addition, it will force you to handle errors properly, and will provide you with a type-safe response object. It uses `openapi-fetch` and you can find more information about it [here](https://openapi-ts.dev/openapi-fetch/). ## 3. Where to put your API queries? The short answer is: **it depends**. There are several factors to consider when deciding where to put your API queries: ### Is the data you are querying related exclusively to a particular component? If so, you should put the query in the component itself. If not, you should consider putting it in a Composable or a Store. ### Can the data be cached? If so, you should consider putting the query in a Store. If not, you should consider putting it in a Composable or the component itself. ### Is the query going to be used in more than one place? If so, you should consider putting it under src/api/.ts and exporting it from there. This will allow you to reuse the query in multiple places specially if you need to do some extra processing of the data. Also it will help to keep track of what parts of the API are being used and where. ### Should I use the `GalaxyApi client` directly or should I write a wrapper function? - If you **don't need to do any additional processing** of the data, you **should** use the `GalaxyApi client` directly. - If you **need to do some additional processing**, then consider writing a wrapper function. Additional processing can be anything from transforming the data to adding extra parameters to the query, providing useful defaults, handling conditional types in response data, etc. - In any case, please try to avoid modeling your own types if you can use the ones defined in the OpenAPI spec (client/src/api/schema/schema.ts). This will help to keep the codebase consistent and avoid duplication of types or API specification drifting.