openapi-vue-query

Fast, type-safe @tanstack/vue-query client to work with your OpenAPI schema.
4,173 6
Install
npm install openapi-vue-query
Latest Version:0.0.5
Vue:3
License:MIT
Last Updated:Apr 27, 2026
Links: GitHub  ·  npm
Maintainer: hirotaka

openapi-vue-query

npm version npm downloads bundle size license

A type-safe and lightweight (1kb) wrapper around @tanstack/vue-query for consuming OpenAPI schemas in Vue.js applications.

Built on top of openapi-fetch and openapi-typescript, providing:

  • ✅ 100% type-safe - No typos in URLs, params, request bodies, or responses
  • ✅ Zero runtime overhead - Types are generated at build time
  • ✅ Automatic type inference - No manual typing of your API
  • ✅ Eliminates any types that hide bugs
  • ✅ Eliminates as type overrides that can hide bugs
  • ✅ Vue 3 Composition API support with full reactivity
  • ✅ All @tanstack/vue-query features - Caching, background updates, optimistic updates, etc.

🚀 Quick Start

Installation

npm i openapi-vue-query openapi-fetch
npm i -D openapi-typescript typescript

Generate Types

Generate TypeScript types from your OpenAPI schema:

npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts

Basic Usage

<script setup lang="ts">
import createFetchClient from "openapi-fetch";
import createClient from "openapi-vue-query";
import type { paths } from "./lib/api/v1"; // generated by openapi-typescript

// Create clients
const fetchClient = createFetchClient<paths>({
  baseUrl: "https://api.example.com/v1",
});
const $api = createClient(fetchClient);

// Use in your components
const { data, error, isPending } = $api.useQuery(
  "get",
  "/blogposts/{post_id}",
  {
    params: {
      path: { post_id: 5 },
    },
  },
);
</script>

<template>
  <div>
    <template v-if="isPending"> Loading... </template>
    <template v-else-if="error"> Error: {{ error.message }} </template>
    <template v-else>
      <h1>{{ data?.title }}</h1>
      <p>{{ data?.content }}</p>
    </template>
  </div>
</template>

🔧 API Reference

useQuery

For fetching data. Wraps @tanstack/vue-query's useQuery.

<script setup lang="ts">
const { data, error, isPending, refetch } = $api.useQuery(
  "get",
  "/users/{id}",
  {
    params: { path: { id: "123" } },
  },
  {
    // Standard vue-query options
    enabled: computed(() => !!userId.value),
    staleTime: 5 * 60 * 1000, // 5 minutes
  },
);
</script>

useMutation

For creating, updating, or deleting data. Wraps @tanstack/vue-query's useMutation.

<script setup lang="ts">
const createPost = $api.useMutation("post", "/posts", {
  onSuccess: () => {
    // Invalidate and refetch posts
    queryClient.invalidateQueries({ queryKey: ["get", "/posts"] });
  },
});

const handleSubmit = () => {
  createPost.mutate({
    body: {
      title: "New Post",
      content: "Post content...",
    },
  });
};
</script>

<template>
  <button @click="handleSubmit" :disabled="createPost.isPending">
    {{ createPost.isPending ? "Creating..." : "Create Post" }}
  </button>
</template>

useInfiniteQuery

For paginated data. Wraps @tanstack/vue-query's useInfiniteQuery.

<script setup lang="ts">
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  $api.useInfiniteQuery(
    "get",
    "/posts",
    {
      params: {
        query: { limit: 10 },
      },
    },
    {
      getNextPageParam: (lastPage, allPages) => {
        return lastPage.hasMore ? allPages.length : undefined;
      },
      pageParamName: "page", // Query parameter name for pagination
    },
  );
</script>

<template>
  <div>
    <div v-for="page in data?.pages" :key="page.page">
      <article v-for="post in page.posts" :key="post.id">
        <h2>{{ post.title }}</h2>
        <p>{{ post.excerpt }}</p>
      </article>
    </div>

    <button
      v-if="hasNextPage"
      @click="fetchNextPage"
      :disabled="isFetchingNextPage"
    >
      {{ isFetchingNextPage ? "Loading..." : "Load More" }}
    </button>
  </div>
</template>

queryOptions

For creating reusable query configurations.

// composables/usePostQuery.ts
export function usePostQuery(postId: Ref<string>) {
  return $api.queryOptions("get", "/posts/{id}", {
    params: { path: { id: postId } },
  });
}

// In component
const postQuery = usePostQuery(postId);
const { data } = useQuery(postQuery);

🎯 Advanced Usage

With Composables

Create reusable API composables:

// composables/useAuth.ts
export function useAuth() {
  const login = $api.useMutation("post", "/auth/login");
  const logout = $api.useMutation("post", "/auth/logout");

  const { data: user } = $api.useQuery("get", "/auth/me", undefined, {
    enabled: computed(() => !!getToken()),
  });

  return {
    user: readonly(user),
    login: login.mutate,
    logout: logout.mutate,
    isLoading: computed(() => login.isPending || logout.isPending),
  };
}

Error Handling

<script setup lang="ts">
const { data, error } = $api.useQuery("get", "/posts/{id}", {
  params: { path: { id: "123" } },
});

// Error is fully typed based on your OpenAPI schema
watchEffect(() => {
  if (error.value) {
    if (error.value.status === 404) {
      console.log("Post not found");
    } else if (error.value.status === 401) {
      console.log("Unauthorized");
    }
  }
});
</script>

Custom Fetch Client Configuration

import createFetchClient from "openapi-fetch";
import createClient from "openapi-vue-query";

const fetchClient = createFetchClient<paths>({
  baseUrl: "https://api.example.com/v1",
  headers: {
    "User-Agent": "MyApp/1.0",
  },
});

// Add auth middleware
fetchClient.use({
  onRequest({ request }) {
    const token = getToken();
    if (token) {
      request.headers.set("Authorization", `Bearer ${token}`);
    }
  },
  onResponse({ response }) {
    if (response.status === 401) {
      // Handle unauthorized
      logout();
    }
  },
});

export const $api = createClient(fetchClient);

🤝 Integration with Vue Query

openapi-vue-query is fully compatible with @tanstack/vue-query. You can use all vue-query features:

<script setup lang="ts">
import { useQueryClient } from "@tanstack/vue-query";

const queryClient = useQueryClient();

// Prefetch data
onMounted(() => {
  queryClient.prefetchQuery($api.queryOptions("get", "/posts"));
});

// Optimistic updates
const updatePost = $api.useMutation("put", "/posts/{id}", {
  onMutate: async (variables) => {
    await queryClient.cancelQueries({
      queryKey: ["get", "/posts", variables.params.path.id],
    });

    const previousPost = queryClient.getQueryData([
      "get",
      "/posts",
      variables.params.path.id,
    ]);

    queryClient.setQueryData(["get", "/posts", variables.params.path.id], {
      ...previousPost,
      ...variables.body,
    });

    return { previousPost };
  },
  onError: (error, variables, context) => {
    if (context?.previousPost) {
      queryClient.setQueryData(
        ["get", "/posts", variables.params.path.id],
        context.previousPost,
      );
    }
  },
});
</script>

📚 Examples

🔗 Related Projects

📖 Documentation

For detailed documentation and examples, visit: https://openapi-ts-vue.dev/openapi-vue-query/

🐛 Issues & Feedback

Found a bug or have a feature request? Please open an issue on GitHub.

📄 License

MIT License. See LICENSE for details.

Related Packages

openapi-fetch

Fast, type-safe fetch client for your OpenAPI schema. Only 6 kb (min). Works wit...

226,490,942 8,405
@shipworthy/openapi-fetch

Fast, type-safe fetch client for your OpenAPI schema. Only 6 kb (min). Works wit...

0
@qualisero/openapi-endpoint

Type-safe OpenAPI integration for Vue Query

6,354 0
@comet-cli/plugin-documentation

Plugin for generating API documentation

6,405 0
vitepress-openapi-docs

Interactive OpenAPI documentation for VitePress. Lightweight, Vue-native, compos...

3,376 6

Version History

Version Released Vue License
0.0.5 ^3.5.13 MIT
0.0.4 ^3.5.13 MIT
0.0.3 ^3.5.13 MIT
0.0.2 ^3.5.13 MIT
0.0.1 ^3.5.13 MIT