N
Nuxt2w ago
Matt

Data Fetching Composable Advice

We have an Organization entity in our app, and each organization has related Events. We’re expecting to reuse this fetching logic across multiple parts of our app. Fetching the organization data alone is straightforward and nicely encapsulated:
// composables/useOrganization.ts
export function useOrganization(id: MaybeRefOrGetter<string>) {
return useFetch<Organization>(`/organizations/${id}`, {
baseURL,
});
};
// composables/useOrganization.ts
export function useOrganization(id: MaybeRefOrGetter<string>) {
return useFetch<Organization>(`/organizations/${id}`, {
baseURL,
});
};
Usage in a page:
// pages/organizations/[id].vue
const $route = useRoute();
const id = $route.params.id as string;

const { data } = await useOrganization(id);

if (!organization.value) {
throw createError({ statusCode: 404, message: "Page not found" });
}
// pages/organizations/[id].vue
const $route = useRoute();
const id = $route.params.id as string;

const { data } = await useOrganization(id);

if (!organization.value) {
throw createError({ statusCode: 404, message: "Page not found" });
}
Where we’re stuck is how best to fetch and structure related event data. These events are tied to the organization. One idea was to extend the composable like this:
// composables/useOrganization.ts
export function useOrganization(id: MaybeRefOrGetter<string>) {
const orgResponse = useFetch<Organization>(`/organizations/${id}`, {
baseURL,
});

function fetchEvents() {
return useFetch<Event>(`/events?organization=${id}`, {
baseURL,
});
}

return {
...orgResponse,
fetchEvents
};
};
// composables/useOrganization.ts
export function useOrganization(id: MaybeRefOrGetter<string>) {
const orgResponse = useFetch<Organization>(`/organizations/${id}`, {
baseURL,
});

function fetchEvents() {
return useFetch<Event>(`/events?organization=${id}`, {
baseURL,
});
}

return {
...orgResponse,
fetchEvents
};
};
Then use it like this:
// pages/organizations/[id].vue
const { data, fetchEvents } = await useOrganization(id);
const { data: events } = fetchEvents();
// pages/organizations/[id].vue
const { data, fetchEvents } = await useOrganization(id);
const { data: events } = fetchEvents();
This works, but by spreading the orgResponse, we lose the “thenable” behavior of useFetch. We’d like to keep the ability to await useOrganization(id) in page-level usage. So I’m wondering: - Is this an acceptable approach? - Should we separate event logic into its own composable (e.g., useOrganizationEvents(id))? - Is there a clean way to preserve the thenable behavior while extending the returned object? Any best practices, patterns, or anti-patterns to consider would be much appreciated!
5 Replies
kapa.ai
kapa.ai2w ago
Hi @Matt. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Your approach is thoughtful, but there are some best practices and Nuxt-specific patterns to consider for composable design and data fetching, especially when dealing with related data from different APIs. 1. Separation of Concerns: Use Separate Composables The recommended pattern in Nuxt is to keep composables focused and reusable. Since events are a distinct resource (and come from a different API), it’s best to create a separate composable for fetching organization events, e.g., useOrganizationEvents(id). This keeps your logic modular, easier to test, and more flexible for future changes or reuse elsewhere in your app. Example:
// composables/useOrganizationEvents.ts
export function useOrganizationEvents(orgId: MaybeRefOrGetter<string>) {
return useFetch<Event[]>(`/events?organization=${orgId}`, {
baseURL: otherApiBaseURL,
});
}
// composables/useOrganizationEvents.ts
export function useOrganizationEvents(orgId: MaybeRefOrGetter<string>) {
return useFetch<Event[]>(`/events?organization=${orgId}`, {
baseURL: otherApiBaseURL,
});
}
Usage:
const { data: organization } = await useOrganization(id);
const { data: events } = await useOrganizationEvents(id);
const { data: organization } = await useOrganization(id);
const { data: events } = await useOrganizationEvents(id);
This approach aligns with Nuxt’s best practices for composables and avoids coupling unrelated fetching logic together [Nuxt useFetch API docs]. 2. Preserving Thenable Behavior When you spread the result of useFetch (or useAsyncData), you lose the thenable/awaitable behavior because the returned object is no longer a Promise. Nuxt’s documentation and community discussions recommend not wrapping or extending the returned object from useFetch if you want to preserve its thenable nature [Nuxt useFetch API docs]. If you need to fetch both organization and events together, you can do so at the page level using Promise.all or similar, but keep the composables themselves focused. 3. Anti-patterns to Avoid - Don’t combine unrelated fetches in a single composable just for convenience. This can make your code harder to maintain and test. - Don’t await inside a custom composable that wraps useFetch or useAsyncData—this can cause unexpected behavior, as noted in the Nuxt docs [Custom useFetch recipe]. 4. If You Need to Fetch Both Together If you have a use case where you always need both organization and events together, you can create a higher-level composable or utility that fetches both, but it should do so by calling the two focused composables internally:
export async function useOrganizationWithEvents(id: MaybeRefOrGetter<string>) {
const [org, events] = await Promise.all([
useOrganization(id),
useOrganizationEvents(id)
]);
return { org, events };
}
export async function useOrganizationWithEvents(id: MaybeRefOrGetter<string>) {
const [org, events] = await Promise.all([
useOrganization(id),
useOrganizationEvents(id)
]);
return { org, events };
}
But for most cases, keeping them separate is preferred. --- Summary:
- Use separate composables for organization and events. - Don’t spread/extend the result of useFetch if you want to keep thenable/awaitable behavior. - Compose at the usage level (page/component) if you need both datasets together. - This approach is in line with Nuxt’s best practices and will keep your codebase clean and maintainable [Nuxt useFetch API docs]. If you need more advanced patterns (like custom fetchers or repository patterns), Nuxt supports those as well, but the above is the recommended starting point [Custom useFetch recipe]. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@1235988804653940826> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Matt
MattOP2w ago
@kapa.ai Would your recommendation change if both Organization and Event data came from the same API? I updated my original post to remove the part about separate APIs.
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- If both Organization and Event data come from the same API, the core recommendation remains the same: separate composables for each resource are still the best practice in Nuxt. This approach keeps your code modular, reusable, and easier to maintain. The fact that both resources are fetched from the same API does not change the composable structure recommended by Nuxt documentation. You can still have:
// composables/useOrganization.ts
export function useOrganization(id: MaybeRefOrGetter<string>) {
return useFetch<Organization>(`/organizations/${id}`, { baseURL });
}

// composables/useOrganizationEvents.ts
export function useOrganizationEvents(orgId: MaybeRefOrGetter<string>) {
return useFetch<Event[]>(`/events?organization=${orgId}`, { baseURL });
}
// composables/useOrganization.ts
export function useOrganization(id: MaybeRefOrGetter<string>) {
return useFetch<Organization>(`/organizations/${id}`, { baseURL });
}

// composables/useOrganizationEvents.ts
export function useOrganizationEvents(orgId: MaybeRefOrGetter<string>) {
return useFetch<Event[]>(`/events?organization=${orgId}`, { baseURL });
}
And use them together in your page or component:
const { data: organization } = await useOrganization(id);
const { data: events } = await useOrganizationEvents(id);
const { data: organization } = await useOrganization(id);
const { data: events } = await useOrganizationEvents(id);
This pattern is explicitly supported and encouraged in the Nuxt documentation, which emphasizes keeping composables focused and using them together as needed in your components or pages. There is no indication in the official docs that combining them into a single composable is preferred, even if the API endpoint is the same. In fact, the docs show examples of using multiple composables for related data, and recommend composing them at the usage level for clarity and maintainability [Nuxt useFetch API docs]. Summary:
- Keep composables focused on a single resource, regardless of whether they share an API. - Compose them together in your page/component as needed. - This approach is in line with Nuxt’s best practices and ensures maintainability and reusability. If you ever need to fetch both datasets in a single request for performance reasons, you can create a new composable for that specific use case, but for most scenarios, separate composables are preferred [Nuxt useFetch API docs]. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@1235988804653940826> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?