Frontend Composables
Frontend Composables
Nuxt Auto API provides TanStack Query-powered composables for seamless data fetching, caching, and mutations on the frontend.
Table of Contents
Installation
The composables are automatically available when you install @websideproject/nuxt-auto-api. They use TanStack Query (Vue Query) under the hood.
npm install @websideproject/nuxt-auto-api
Query Composables
useAutoApiList
Fetch a list of resources with automatic caching and revalidation.
Basic Usage
<script setup lang="ts">
const { data, isLoading, error, refetch } = useAutoApiList('posts')
</script>
<template>
<div>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else-if="data">
<article v-for="post in data.data" :key="post.id">
<h2>{{ post.title }}</h2>
</article>
</div>
</div>
</template>
With Filters
<script setup lang="ts">
const route = useRoute()
const { data } = useAutoApiList('posts', {
filter: {
status: route.query.status || 'published',
userId: route.query.author
},
sort: '-createdAt',
limit: 10
})
</script>
With Relations
<script setup lang="ts">
const { data } = useAutoApiList('posts', {
include: 'author,comments',
filter: { published: true }
})
</script>
<template>
<article v-for="post in data?.data" :key="post.id">
<h2>{{ post.title }}</h2>
<p>By {{ post.author.name }}</p>
<p>{{ post.comments.length }} comments</p>
</article>
</template>
API Reference
function useAutoApiList<T>(
resource: MaybeRef<string>,
params?: MaybeRef<ListQueryParams>,
options?: UseQueryOptions
)
interface ListQueryParams {
filter?: Record<string, any> // Filtering
sort?: string | string[] // Sorting (-field for desc)
page?: number // Page number
limit?: number // Items per page
include?: string | string[] // Relations to include
fields?: string | string[] // Select specific fields
}
Return Value
{
data: Ref<ListResponse<T>> // { data: T[], meta: { ... } }
isLoading: Ref<boolean>
error: Ref<Error | null>
refetch: () => void
// ... other TanStack Query properties
}
useAutoApiGet
Fetch a single resource by ID.
Basic Usage
<script setup lang="ts">
const route = useRoute()
const postId = route.params.id
const { data, isLoading } = useAutoApiGet('posts', postId)
</script>
<template>
<div v-if="isLoading">Loading...</div>
<article v-else-if="data">
<h1>{{ data.data.title }}</h1>
<p>{{ data.data.content }}</p>
</article>
</template>
With Relations
<script setup lang="ts">
const { data } = useAutoApiGet('posts', postId, {
include: 'author,comments'
})
</script>
<template>
<article v-if="data">
<h1>{{ data.data.title }}</h1>
<p>By {{ data.data.author.name }}</p>
<div v-for="comment in data.data.comments" :key="comment.id">
{{ comment.content }}
</div>
</article>
</template>
API Reference
function useAutoApiGet<T>(
resource: MaybeRef<string>,
id: MaybeRef<string | number>,
params?: MaybeRef<{
include?: string | string[]
fields?: string | string[]
}>,
options?: UseQueryOptions
)
useAutoApiInfinite
Infinite scroll pagination with cursor-based loading.
Basic Usage
<script setup lang="ts">
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useAutoApiInfinite('posts', {
limit: 20,
sort: '-createdAt'
})
const allPosts = computed(() => {
return data.value?.pages.flatMap(page => page.data) ?? []
})
</script>
<template>
<div>
<article v-for="post in allPosts" :key="post.id">
<h2>{{ post.title }}</h2>
</article>
<button
v-if="hasNextPage"
@click="fetchNextPage"
:disabled="isFetchingNextPage"
>
{{ isFetchingNextPage ? 'Loading...' : 'Load More' }}
</button>
</div>
</template>
With Intersection Observer
<script setup lang="ts">
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useAutoApiInfinite('posts', {
limit: 20
})
const allPosts = computed(() => data.value?.pages.flatMap(page => page.data) ?? [])
const loadMoreRef = ref<HTMLElement>()
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage.value && !isFetchingNextPage.value) {
fetchNextPage()
}
},
{ threshold: 0.1 }
)
if (loadMoreRef.value) {
observer.observe(loadMoreRef.value)
}
onUnmounted(() => observer.disconnect())
})
</script>
<template>
<div>
<article v-for="post in allPosts" :key="post.id">
<h2>{{ post.title }}</h2>
</article>
<div ref="loadMoreRef" v-if="hasNextPage">
<div v-if="isFetchingNextPage">Loading more...</div>
</div>
</div>
</template>
Mutation Composables
useAutoApiMutation
Unified mutation API that automatically dispatches to the correct operation (create, update, or delete). This is the recommended approach for most use cases as it provides a cleaner, more consistent API.
Basic Usage
<script setup lang="ts">
const toast = useToast()
// Create mutation
const { mutateAsync: createPost } = useAutoApiMutation('posts', 'create', {
toast: {
success: { title: 'Post created!' },
error: { title: 'Failed to create post' }
}
})
// Update mutation
const { mutateAsync: updatePost } = useAutoApiMutation('posts', 'update', {
toast: {
success: { title: 'Post updated!' },
error: { title: 'Failed to update post' }
}
})
// Delete mutation
const { mutateAsync: deletePost } = useAutoApiMutation('posts', 'delete', {
toast: {
success: { title: 'Post deleted!' },
error: { title: 'Failed to delete post' }
}
})
// Usage
const handleCreate = async () => {
await createPost({
title: 'New Post',
content: 'Content here'
})
}
const handleUpdate = async (postId: number) => {
await updatePost({
id: postId,
data: {
title: 'Updated Title'
}
})
}
const handleDelete = async (postId: number) => {
await deletePost(postId)
}
</script>
With Toast Notifications
The mutation composables integrate with Nuxt UI's toast system:
<script setup lang="ts">
const { mutateAsync: createPost, isPending } = useAutoApiMutation('posts', 'create', {
toast: {
success: {
title: 'Success!',
description: 'Your post has been created'
},
error: {
title: 'Error',
description: 'Failed to create post'
}
}
})
const handleSubmit = async () => {
try {
await createPost(formData.value)
// Toast automatically shown on success
navigateTo('/posts')
} catch (err) {
// Toast automatically shown on error
console.error(err)
}
}
</script>
API Reference
function useAutoApiMutation<T, TBody>(
resource: MaybeRef<string>,
action: 'create' | 'update' | 'delete',
options?: {
toast?: {
success?: { title: string; description?: string }
error?: { title: string; description?: string }
}
onSuccess?: (data: any, variables: any, context: any) => void
onError?: (error: Error, variables: any, context: any) => void
// ... other TanStack Query mutation options
}
)
Return Value
{
mutate: (variables: TVariables) => void
mutateAsync: (variables: TVariables) => Promise<TData>
isPending: Ref<boolean>
isSuccess: Ref<boolean>
isError: Ref<boolean>
error: Ref<Error | null>
data: Ref<TData | undefined>
reset: () => void
// ... other TanStack Query mutation properties
}
useAutoApiCreate
Create a new resource with automatic cache invalidation.
Note: Consider using the unified
useAutoApiMutation('resource', 'create')API for a more consistent interface. This section documents the direct API.
Basic Usage
<script setup lang="ts">
const router = useRouter()
const { mutate, isPending, error } = useAutoApiCreate('posts', {
onSuccess: (response) => {
// List cache automatically invalidated
router.push(`/posts/${response.data.id}`)
}
})
const form = ref({
title: '',
content: '',
userId: 1
})
const handleSubmit = () => {
mutate(form.value)
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.title" placeholder="Title" required />
<textarea v-model="form.content" placeholder="Content" />
<button :disabled="isPending">
{{ isPending ? 'Creating...' : 'Create Post' }}
</button>
<div v-if="error" class="error">{{ error.message }}</div>
</form>
</template>
API Reference
function useAutoApiCreate<T, TBody>(
resource: MaybeRef<string>,
options?: UseMutationOptions<GetResponse<T>, Error, TBody>
)
useAutoApiUpdate
Update an existing resource with cache invalidation.
Note: Consider using the unified
useAutoApiMutation('resource', 'update')API for a more consistent interface. This section documents the direct API.
Basic Usage
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
const postId = route.params.id
// Fetch existing post
const { data: post } = useAutoApiGet('posts', postId)
// Update mutation
const { mutate, isPending } = useAutoApiUpdate('posts', {
onSuccess: () => {
router.push(`/posts/${postId}`)
}
})
const form = ref<any>({})
watch(() => post.value?.data, (data) => {
if (data) {
form.value = { ...data }
}
}, { immediate: true })
const handleSubmit = () => {
mutate({
id: postId,
title: form.value.title,
content: form.value.content
})
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.title" />
<textarea v-model="form.content" />
<button :disabled="isPending">
{{ isPending ? 'Saving...' : 'Save Changes' }}
</button>
</form>
</template>
useAutoApiDelete
Delete a resource with cache cleanup.
Note: Consider using the unified
useAutoApiMutation('resource', 'delete')API for a more consistent interface. This section documents the direct API.
Basic Usage
<script setup lang="ts">
const router = useRouter()
const { mutate, isPending } = useAutoApiDelete('posts', {
onSuccess: () => {
router.push('/posts')
}
})
const handleDelete = (postId: number) => {
if (confirm('Are you sure?')) {
mutate(postId)
}
}
</script>
<template>
<button
@click="handleDelete(post.id)"
:disabled="isPending"
>
{{ isPending ? 'Deleting...' : 'Delete' }}
</button>
</template>
Advanced Patterns
Optimistic Updates
Update the UI immediately before the server responds, then rollback on error.
<script setup lang="ts">
const queryClient = useQueryClient()
const { mutate } = useAutoApiUpdate('posts', {
onMutate: async (variables) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({
queryKey: ['autoapi', 'posts', 'get', variables.id]
})
// Snapshot previous value
const previousData = queryClient.getQueryData(['autoapi', 'posts', 'get', variables.id])
// Optimistically update
queryClient.setQueryData(['autoapi', 'posts', 'get', variables.id], (old: any) => ({
data: { ...old.data, ...variables }
}))
return { previousData }
},
onError: (err, variables, context) => {
// Rollback on error
if (context?.previousData) {
queryClient.setQueryData(
['autoapi', 'posts', 'get', variables.id],
context.previousData
)
}
}
})
</script>
Or use the helper:
<script setup lang="ts">
const { mutate } = useAutoApiUpdate('posts', {
onMutate: async (variables) => {
return useAutoApiOptimisticUpdate('posts', variables.id, variables)
},
onError: (err, variables, context) => {
if (context) {
queryClient.setQueryData(context.queryKey, context.previousData)
}
}
})
</script>
Infinite Scroll
<script setup lang="ts">
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useAutoApiInfinite('posts', {
limit: 20,
filter: { published: true },
sort: '-createdAt'
})
const allPosts = computed(() => {
return data.value?.pages.flatMap(page => page.data) ?? []
})
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement
if (scrollTop + clientHeight >= scrollHeight - 100) {
if (hasNextPage.value && !isFetchingNextPage.value) {
fetchNextPage()
}
}
}
onMounted(() => {
window.addEventListener('scroll', handleScroll)
onUnmounted(() => window.removeEventListener('scroll', handleScroll))
})
</script>
Server-Side Rendering
Composables work seamlessly with Nuxt's SSR.
<script setup lang="ts">
// Data will be prefetched on server and hydrated on client
const { data, isLoading } = useAutoApiList('posts', {
filter: { published: true },
sort: '-createdAt',
limit: 10
}, {
staleTime: 1000 * 60 * 5 // 5 minutes
})
</script>
<template>
<!-- Posts rendered on server, cached on client -->
<div v-if="data">
<article v-for="post in data.data" :key="post.id">
<h2>{{ post.title }}</h2>
</article>
</div>
</template>
Error Handling
All composables return error states for handling failures.
<script setup lang="ts">
const { data, error, isError } = useAutoApiList('posts')
const { mutate, error: createError } = useAutoApiCreate('posts', {
onError: (error) => {
console.error('Failed to create post:', error)
// Show toast notification
}
})
</script>
<template>
<div v-if="isError">
<p>Failed to load posts: {{ error.message }}</p>
<button @click="refetch">Retry</button>
</div>
<form @submit.prevent="handleSubmit">
<!-- ... -->
<p v-if="createError" class="error">
{{ createError.message }}
</p>
</form>
</template>
TypeScript Support
All composables are fully typed.
<script setup lang="ts">
interface Post {
id: number
title: string
content: string
userId: number
author?: User
}
interface User {
id: number
name: string
email: string
}
// Typed response
const { data } = useAutoApiList<Post>('posts', {
include: 'author'
})
// Type-safe access
const firstPost = computed(() => data.value?.data[0])
// firstPost.value.title ✅
// firstPost.value.author.name ✅
// firstPost.value.nonexistent ❌ Type error
// Typed mutations
const { mutate } = useAutoApiCreate<Post, Partial<Post>>('posts')
mutate({
title: 'New Post',
content: 'Content',
userId: 1
}) // ✅
mutate({
invalid: 'field'
}) // ❌ Type error
</script>
Cache Configuration
Customize TanStack Query behavior globally.
// plugins/query-config.ts
export default defineNuxtPlugin(() => {
const queryClient = useNuxtApp().$queryClient
queryClient.setDefaultOptions({
queries: {
staleTime: 1000 * 60 * 10, // 10 minutes
gcTime: 1000 * 60 * 30, // 30 minutes
retry: 3
}
})
})
Best Practices
- Use the unified mutation API - Prefer
useAutoApiMutationover individual mutation composables for consistency - Use reactive params - Pass refs to make queries reactive
- Enable/disable queries - Use
enabledoption for conditional fetching - Prefetch data - Use
prefetchQueryfor better UX - Invalidate strategically - Let mutations auto-invalidate related queries
- Handle loading states - Show skeletons or spinners
- Type your data - Use TypeScript for better DX
- Cache wisely - Set appropriate
staleTimeandgcTime - Optimize re-renders - Use
computedfor derived state - Use toast notifications - Enable automatic toast notifications for mutations to improve UX
Resources
SQLite to D1 Migration
This guide walks you through migrating your Nuxt Auto API application from local SQLite to Cloudflare D1 for edge deployment.
Testing
This guide covers how to write and run tests for Nuxt Auto API, including unit tests, integration tests, and E2E tests with both SQLite and Cloudflare D1.