Vue Query is a server state management library for VueJs. This tutorial will teach you how to build a Vue.js, Vue Query, and Axios CRUD application with RESTful API to perform the Create/Update/Get/Delete operations.

More practice:

Build Vue.js, Vue Query, and Axios CRUD App with RESTful API

Benefits of using Vue Query in Vue.js

Vue Query is a package that provides hooks for fetching, caching, synchronizing, and updating server data in Vue. The library inherited all its main concepts from React Query so feel free to check out the React Query documentation.

By default, Vue does not provide an opinionated way of working with asynchronous data. So developers find themselves building custom workarounds that end up either over-complicated or feature lacking.

Below are some benefits of using Vue Query in Vue:

  • Memory and Garbage collection are carefully managed
  • Caching and memoizing query results
  • Performance optimization when paginating and lazy loading data
  • Invalidate “out of date” cached data and request a fresh one in the background

Vue Query and Axios Overview

We will build a Vue.js, Tailwind CSS, and Typescript client with Vue Query and Axios to make CRUD operations against a RESTful API:

HTTP METHODROUTEDESCRIPTION
GET/api/postsRetrieve all posts
POST/api/postsCreate new post
GET/api/posts/:idGet a single post
PATCH/api/posts/:idUpdates a post
DELETE/api/posts/:idDeletes a post

-On the homepage, a Vue Query Axios GET request is made to retrieve all the posts from the server

vue query axios get request to retrieve all posts

-To create a new post, click on “Create Post” from the navigation menu to display the create post modal.

Next, provide the necessary information and make a Vue Query Axios POST request to the RESTful API by clicking the “Create Post” button.

vue query axios post request to create a post

-To update a post, hover over the three horizontal dots on the post and click on the edit button to display the edit popup.

vue query axios edit post

When the edit popup opens, the input fields will be automatically filled. After adding the changes, make a Vue Query Axios PATCH request to update the post on the server by clicking the “Edit Post” button.

vue query axios patch request to update post

-Finally, to delete a post, hover over the three dots again and click on the delete button. You will be prompted to confirm your action before a Vue Query Axios DELETE request will be made to remove that post from the database.

vue query axios delete request to delete a post

Follow one of these tutorials to build the CRUD RESTful API:

Install Vue Query with Axios and Other Dependencies

Run the following commands in your preferred terminal to generate a Vue.js boilerplate application in a vue-query-axios directory.


npm init vue@latest

Running this command will install and execute the Vue project scaffolding tool (Create-Vue) and you will be prompted for a number of optional features like Typescript, Vue Router, Pinia, Vuex, and testing support:

generate a vuejs project with vite

Select Yes for Vue Router, Typescript, and Pinia to make the scaffolding tool include them in the project.

Setup tailwindCss in Vue

Install tailwindCss and its dependencies

First and foremost, open the Vue project with your preferred text editor and install tailwindCss and its peer dependencies via npm or yarn.


npm install -D tailwindcss postcss autoprefixer
# or
yarn add -D tailwindcss postcss autoprefixer

Run the tailwindCss init command to create both tailwind.config.js and postcss.config.js files.


npx tailwindcss init -p

Configure your template paths

Next, add the paths to your template files in the tailwind.config.js file, and feel free to include your custom colors and fonts.

tailwind.config.js


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        'ct-dark-600': '#222',
        'ct-dark-200': '#e5e7eb',
        'ct-dark-100': '#f5f6f7',
        'ct-blue-600': '#2363eb',
        'ct-yellow-600': '#f9d13e',
      },
      fontFamily: {
        Poppins: ['Poppins, sans-serif'],
      },
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },
  plugins: [],
};

Add the tailwindCss directives to your CSS

Now, create a ./src/index.css file and add the @tailwind directives. Also, remember to include your font to override the default tailwindCss font.


@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

Import the CSS file

Now import the ./src/index.css file into the ./src/main.ts file.

src/main.ts


import { createApp } from 'vue';
import { createPinia } from 'pinia';
import './index.css';

import App from './App.vue';
import router from './router';

const app = createApp(App);

app.use(createPinia());
app.use(router);

app.mount('#app');

Install Vue Query, Axios, Vee-Validate, and Zod


yarn add vue-query axios vee-validate zod @vee-validate/zod mosha-vue-toastify 
# or 
npm install vue-query axios vee-validate zod @vee-validate/zod mosha-vue-toastify
  • vue-query – For managing server state in Vue.js
  • axios – For making asynchronous HTTP requests
  • vee-validate – For synchronous, asynchronous, field-level, or form-level validation.
  • mosha-vue-toastify – For displaying alerts
  • zod – A schema declaration and validation library

Next, add vue-query to the middleware stack in ./src/main.ts file and import the mosha-vue-toastify CSS.

src/main.ts


import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { VueQueryPlugin } from 'vue-query';
import './index.css';
import 'mosha-vue-toastify/dist/style.css';

import App from './App.vue';
import router from './router';

const app = createApp(App);

app.use(createPinia());
app.use(router);
app.use(VueQueryPlugin);

app.mount('#app');

Out of the box“, Vue Query works great with zero-config and can be customized to meet your needs as your application grows.

By default, Vue Query considers queries as stale staleTime: 0 and the stale queries are prefetched in the background when:

  • the window is refocused
  • the network reconnects
  • a query has a re-fetch interval
  • new instances of the query mounts

You can configure the queries globally or per query to change the default behavior.

Create the Request and Response Types

Now that we are done installing the necessary packages, create and export the request and response interfaces in the ./src/api/types.ts file.

I assume you’ve already implemented the authentication part in Vue.js, Pinia, Vue Query, Axios Interceptors JWT Authentication.

The RESTful API requires you to be authenticated before you can perform the CRUD operations.

src/api/types.ts


export interface IUser {
  name: string;
  email: string;
  role: string;
  photo: string;
  _id: string;
  id: string;
  created_at: string;
  updated_at: string;
  __v: number;
}

export interface GenericResponse {
  status: string;
  message: string;
}

export interface ILoginInput {
  email: string;
  password: string;
}

export interface ISignUpInput {
  name: string;
  email: string;
  password: string;
  passwordConfirm: string;
}

export interface ILoginResponse {
  status: string;
  access_token: string;
}

export interface ISignUpResponse {
  status: string;
  message: string;
}

export interface IUserResponse {
  status: string;
  data: {
    user: IUser;
  };
}

export interface IPostRequest {
  title: string;
  content: string;
  image: string;
  user: string;
}

export interface IPostResponse {
  id: string;
  title: string;
  content: string;
  image: string;
  category: string;
  user: IUser;
  created_at: string;
  updated_at: string;
}

export interface IPostsResponse {
  status: string;
  data: {
    posts: IPostResponse[];
  };
}

Create the API Services with Axios

Create a src/api/postApi.ts file and add the following code to create the CRUD (CREATE, READ, UPDATE, and DELETE) services.

src/api/postApi.ts


import { authApi } from './authApi';
import type { GenericResponse, IPostResponse, IPostsResponse } from './types';

export const getAllPostsFn = async () => {
  const response = await authApi.get<IPostsResponse>(`posts`);
  return response.data;
};

export const getPostFn = async (id: string) => {
  const response = await authApi.get<IPostResponse>(`posts/${id}`);
  return response.data;
};

export const createPostFn = async (formData: FormData) => {
  const response = await authApi.post<IPostResponse>(`posts`, formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
  });
  return response.data;
};

export const updatePostFn = async ({
  id,
  formData,
}: {
  id: string;
  formData: FormData;
}) => {
  const response = await authApi.patch<IPostResponse>(`posts/${id}`, formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
  });
  return response.data;
};

export const deletePostFn = async (id: string) => {
  const response = await authApi.delete<GenericResponse>(`posts/${id}`);
  return response.data;
};
  • getAllPostsFn – Makes an Axios GET request to get all the posts
  • getPostFn – Makes an Axios GET request to get a single post
  • createPostFn – Makes an Axios POST request to create a new post
  • updatePostFn – Makes a PATCH request to update a post
  • deletePostFn – Makes a DELETE request to delete a post from the database.

Note: Set the Content-Type to 'multipart/form-data' for the CREATE and UPDATE CRUD services to enable Axios to send the form data to the server.

Vue Query and Axios POST Request

To mutate data on the server (CREATE, PATCH, PUT, or DELETE operations), we use the Vue Query useMutation hook.


const {
  mutate: createPost,
  isLoading,
  isError,
  data,
  error,
  isSuccess,
} = useMutation((formData: FormData) => createPostFn(formData), {
  onSuccess: (data) => {},
  onError: (error: any) => {},
});
  • createPostFn (mutationFn): a function to mutate the data.
  • onSuccess : a function that will be called when the mutation is successful and receives the data returned as a parameter.
  • onError : a function that will be called when the mutation encounters an error.

The object returned by the useMutation() hook has the following properties:

  • mutate : a function you can evoke to manually trigger the mutation
  • isLoading : a boolean that indicates whether the mutation is currently executing.
  • isError : a boolean that indicates whether the mutation resulted in an error.
  • error : the object returned when the mutation results in an error.
  • data : the data returned when the mutation is successful.
  • isSuccess : a boolean that indicates whether the mutation was successful.

You can visit the official Vue Query website to learn more about the useMutation() hook.

Note: After running each of the mutations, you need to invalidate the query cache so that Vue Query can re-fetch the most recent data from the database.


import { useQueryClient } from 'vue-query';
const queryClient = useQueryClient();
queryClient.invalidateQueries('posts');

Create Reusable Components with tailwindCss

Let’s create a custom tailwindCss button to have a loading spinner.

src/components/icons/Spinner.vue


<template>
  <svg
    role="status"
    :class="`w-${width} h-${height} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`"
    viewBox="0 0 100 101"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path
      d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
      fill="currentColor"
    />
    <path
      d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
      fill="currentFill"
    />
  </svg>
</template>

<script setup lang="ts">
defineProps({
  width: {
    type: Number,
    default: 5,
  },
  height: {
    type: Number,
    default: 5,
  },
});
</script>

src/components/LoadingButton.vue


<template>
  <button
    type="submit"
    :class="`w-full py-3 font-semibold ${btnColor} rounded-lg outline-none border-none flex justify-center ${
      loading ? 'bg-[#ccc]' : ''
    }`"
    :disabled="loading"
  >
    <div v-if="loading" class="flex items-center gap-3">
      <Spinner />
      <span class="text-slate-500">Loading...</span>
    </div>
    <span v-else :class="`${textColor}`"><slot></slot></span>
  </button>
</template>

<script setup lang="ts">
import Spinner from '@/components/icons/Spinner.vue';
defineProps({
  loading: {
    type: Boolean,
    default: false,
  },
  btnColor: {
    type: String,
    default: 'bg-ct-yellow-600',
  },
  textColor: {
    type: String,
    default: 'text-white',
  },
});
</script>

With that out of the way, let’s use Vue Query and Axios to add a new post to the database.

src/components/CreatePost.vue


<template>
  <section>
    <div
      class="fixed inset-0 bg-[rgba(0,0,0,.3)] z-[1000]"
      @click="toggleModal"
    ></div>
    <div
      class="max-w-lg w-full rounded-md fixed top-[15%] left-1/2 -translate-x-1/2 bg-white z-[1001] p-6"
    >
      <h2 class="text-2xl font-semibold mb-4">Create Post</h2>

      <form class="w-full" @submit="onSubmit">
        <div class="mb-2">
          <label class="block text-gray-700 text-lg mb-2" for="title">
            Title
          </label>
          <input
            :class="`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none ${
              errors.title ? 'border-red-500' : ''
            }`"
            id="title"
            type="text"
            placeholder=" "
            v-model="title"
          />
          <p
            :class="`text-red-500 text-xs italic mb-2 ${
              errors.title ? 'visible' : 'invisible'
            }`"
          >
            {{ errors.title }}
          </p>
        </div>
        <div class="mb-2">
          <label class="block text-gray-700 text-lg mb-2" for="title">
            Category
          </label>
          <input
            :class="`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none ${
              errors.category ? 'border-red-500' : ''
            }`"
            id="category"
            type="text"
            placeholder=" "
            v-model="category"
          />
          <p
            :class="`text-red-500 text-xs italic mb-2 ${
              errors.category ? 'visible' : 'invisible'
            }`"
          >
            {{ errors.category }}
          </p>
        </div>
        <div class="mb-2">
          <label class="block text-gray-700 text-lg mb-2" for="title">
            Content
          </label>
          <textarea
            :class="`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none ${
              errors.content ? 'border-red-500' : ''
            }`"
            id="content"
            rows="4"
            placeholder=" "
            v-model="content"
          />
          <p
            :class="`text-red-500 text-xs italic mb-2 ${
              errors.content ? 'visible' : 'invisible'
            }`"
          >
            {{ errors.content }}
          </p>
        </div>
        <div class="mb-2">
          <span class="sr-only">Choose profile photo</span>
          <Field name="image" v-slot="{ errorMessage }">
            <input
              @change="onFileChange"
              @blur="onFileChange"
              type="file"
              class="block text-sm mb-2 text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-violet-50 file:text-violet-700 hover:file:bg-violet-100"
            />
            <p
              :class="`text-red-500 text-xs italic mb-2 ${
                errorMessage ? 'visible' : 'invisible'
              }`"
            >
              {{ errorMessage }}
            </p>
          </Field>
        </div>
        <LoadingButton :loading="isLoading" :btnColor="'bg-ct-blue-600'"
          >Create Post</LoadingButton
        >
      </form>
    </div>
  </section>
</template>

<script setup lang="ts">
import { useField, useForm, Field } from 'vee-validate';
import * as zod from 'zod';
import { toFormValidator } from '@vee-validate/zod';
import { useMutation, useQueryClient } from 'vue-query';
import { createPostFn } from '@/api/postApi';
import { createToast } from 'mosha-vue-toastify';
import { toRefs } from 'vue';
import LoadingButton from '@/components/LoadingButton.vue';

const props = defineProps<{
  toggleModal: () => void;
}>();

const { toggleModal } = toRefs(props);

const createPostSchema = toFormValidator(
  zod.object({
    title: zod.string().min(1, 'Title is required'),
    category: zod.string().min(1, 'Category is required'),
    content: zod.string().min(1, 'Content is required'),
    image: zod.instanceof(File),
  })
);

const { handleSubmit, errors, resetForm } = useForm({
  validationSchema: createPostSchema,
});

const { value: title } = useField<string>('title');
const { value: category } = useField<string>('category');
const { value: content } = useField<string>('content');
const { setValue } = useField<File>('image');

const onFileChange = (event: any) => {
  let files: FileList = event.target.files;
  const file = files[0];
  if (file) {
    setValue(file);
  }
};

const queryClient = useQueryClient();
const { mutate: createPost, isLoading } = useMutation(
  (formData: FormData) => createPostFn(formData),
  {
    onSuccess: (data) => {
      createToast('Post created successfully', {
        position: 'top-right',
      });
      queryClient.invalidateQueries('posts');
      toggleModal.value();
    },
    onError: (error: any) => {
      toggleModal.value();
      if (Array.isArray((error as any).response.data.error)) {
        (error as any).response.data.error.forEach((el: any) =>
          createToast(el.message, {
            position: 'top-right',
            type: 'warning',
          })
        );
      } else {
        createToast((error as any).response.data.message, {
          position: 'top-right',
          type: 'danger',
        });
      }
    },
  }
);

const onSubmit = handleSubmit((values) => {
  const formData = new FormData();

  formData.append('image', values.image);
  formData.append('data', JSON.stringify(values));
  createPost(formData);
  resetForm();
});
</script>

Vue Query and Axios PATCH Request

Next, let’s create a Vue tailwindCss component to update a post in the database using Vue Query and Axios.


yarn add lodash date-fns && yarn add -D @types/lodash
# or
npm install lodash date-fns && npm install -D @types/lodash

src/components/EditPost.vue


<template>
  <section>
    <div
      class="fixed inset-0 bg-[rgba(0,0,0,.3)] z-[1000]"
      @click="toggleModal"
    ></div>
    <div
      class="max-w-lg w-full rounded-md fixed top-[15%] left-1/2 -translate-x-1/2 bg-white z-[1001] p-6"
    >
      <h2 class="text-2xl font-semibold mb-4">Edit Post</h2>
      <form class="w-full" @submit="onSubmit">
        <div class="mb-2">
          <label class="block text-gray-700 text-lg mb-2" for="title">
            Title
          </label>
          <input
            :class="`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none ${
              errors.title ? 'border-red-500' : ''
            }`"
            id="title"
            type="text"
            placeholder=" "
            v-model="title"
          />
          <p
            :class="`text-red-500 text-xs italic mb-2 ${
              errors.title ? 'visible' : 'invisible'
            }`"
          >
            {{ errors.title }}
          </p>
        </div>
        <div class="mb-2">
          <label class="block text-gray-700 text-lg mb-2" for="title">
            Category
          </label>
          <input
            :class="`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none ${
              errors.category ? 'border-red-500' : ''
            }`"
            id="category"
            type="text"
            placeholder=" "
            v-model="category"
          />
          <p
            :class="`text-red-500 text-xs italic mb-2 ${
              errors.category ? 'visible' : 'invisible'
            }`"
          >
            {{ errors.category }}
          </p>
        </div>
        <div class="mb-2">
          <label class="block text-gray-700 text-lg mb-2" for="title">
            Content
          </label>
          <textarea
            :class="`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none ${
              errors.content ? 'border-red-500' : ''
            }`"
            id="content"
            rows="4"
            placeholder=" "
            v-model="content"
          />
          <p
            :class="`text-red-500 text-xs italic mb-2 ${
              errors.content ? 'visible' : 'invisible'
            }`"
          >
            {{ errors.content }}
          </p>
        </div>
        <div class="mb-2">
          <span class="sr-only">Choose profile photo</span>
          <Field name="image" v-slot="{ errorMessage }">
            <input
              @change="onFileChange"
              @blur="onFileChange"
              type="file"
              class="block text-sm mb-2 text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-violet-50 file:text-violet-700 hover:file:bg-violet-100"
            />
            <p
              :class="`text-red-500 text-xs italic mb-2 ${
                errorMessage ? 'visible' : 'invisible'
              }`"
            >
              {{ errorMessage }}
            </p>
          </Field>
        </div>
        <LoadingButton :loading="isLoading" :btnColor="'bg-ct-blue-600'"
          >Edit Post</LoadingButton
        >
      </form>
    </div>
  </section>
</template>

<script setup lang="ts">
import { onMounted } from 'vue';
import { useField, useForm, Field } from 'vee-validate';
import * as zod from 'zod';
import { toFormValidator } from '@vee-validate/zod';
import { useMutation } from 'vue-query';
import { updatePostFn } from '@/api/postApi';
import { createToast } from 'mosha-vue-toastify';
import type { IPostResponse } from '@/api/types';
import { pickBy } from 'lodash';
import { toRefs } from 'vue';
import LoadingButton from './LoadingButton.vue';

const props = defineProps<{
  toggleModal: () => void;
  post: IPostResponse;
}>();

const { post, toggleModal } = toRefs(props);

const editPostSchema = toFormValidator(
  zod
    .object({
      title: zod.string(),
      category: zod.string(),
      content: zod.string(),
      image: zod.instanceof(File),
    })
    .partial()
);

const { handleSubmit, errors, setFieldValue } = useForm({
  validationSchema: editPostSchema,
});

const { value: title } = useField<string>('title');
const { value: category } = useField<string>('category');
const { value: content } = useField<string>('content');
const { setValue } = useField<File>('image');

const onFileChange = (event: any) => {
  let files: FileList = event.target.files;
  const file = files[0];
  if (file) {
    setValue(file);
  }
};

const { mutate: updatePost, isLoading } = useMutation(
  ({ id, formData }: { id: string; formData: FormData }) =>
    updatePostFn({ id, formData }),
  {
    onSuccess: (data) => {
      createToast('Post updated successfully', {
        position: 'top-right',
      });
      toggleModal.value();
    },
    onError: (error: any) => {
      toggleModal.value();
      if (Array.isArray((error as any).response.data.error)) {
        (error as any).response.data.error.forEach((el: any) =>
          createToast(el.message, {
            position: 'top-right',
            type: 'warning',
          })
        );
      } else {
        createToast((error as any).response.data.message, {
          position: 'top-right',
          type: 'danger',
        });
      }
    },
  }
);

const onSubmit = handleSubmit((values) => {
  const formData = new FormData();
  const filteredFormData = pickBy(
    values,
    (value) => value !== '' && value !== undefined
  );
  const { image, ...otherFormData } = filteredFormData;
  if (image) {
    formData.append('image', image);
  }
  formData.append('data', JSON.stringify(otherFormData));
  updatePost({ id: post.value.id, formData });
});

onMounted(() => {
  setFieldValue('title', post.value.title);
  setFieldValue('category', post.value.category);
  setFieldValue('content', post.value.content);
});
</script>

Vue Query and Axios DELETE Request

Next, let’s use Vue Query and Axios to remove a post from the database.


<template>
  <div class="rounded-md shadow-md bg-white">
    <div class="mx-2 mt-2 overflow-hidden rounded-md">
      <img
        :src="`${VUE_APP_API_URL}/api/static/posts/${post.image}`"
        alt=""
        class="object-fill w-full h-full"
      />
    </div>
    <div class="p-4">
      <h5 class="font-semibold text-xl text-[#4d4d4d] mb-4">
        {{
          post.title.length > 25
            ? post.title.substring(0, 25) + '...'
            : post.title
        }}
      </h5>
      <div class="flex items-center mt-4">
        <p class="p-1 rounded-sm mr-4 bg-[#dad8d8]">{{ post.category }}</p>
        <p class="text-[#ffa238]">
          {{ format(parseISO(post.created_at), 'PPP') }}
        </p>
      </div>
    </div>
    <div class="flex justify-between items-center px-4 pb-4">
      <div class="flex items-center">
        <div class="w-12 h-12 rounded-full overflow-hidden">
          <img
            :src="`${VUE_APP_API_URL}/api/static/users/${post.user.photo}`"
            alt=""
            class="object-cover w-full h-full"
          />
        </div>
        <p class="ml-4 text-sm font-semibold">{{ post.user.name }}</p>
      </div>
      <div class="relative">
        <div
          class="text-3xl text-[#4d4d4d] cursor-pointer p-3"
          @click="showSettings"
        >
          <i class="bx bx-dots-horizontal-rounded"></i>
        </div>
        <ul
          class="absolute bottom-5 -right-1 z-50 py-2 rounded-sm bg-white shadow-lg transition ease-out duration-300"
          v-if="openSettings"
        >
          <li
            class="w-24 h-7 py-3 px-2 hover:bg-[#f5f5f5] flex items-center gap-2 cursor-pointer transition ease-in duration-300"
            @click="toggleModal"
          >
            <i class="bx bx-edit-alt"></i> <span>Edit</span>
          </li>
          <li
            class="w-24 h-7 py-3 px-2 hover:bg-[#f5f5f5] flex items-center gap-2 cursor-pointer transition ease-in duration-300"
            @click="onDelete"
          >
            <i class="bx bx-trash"></i> <span>Delete</span>
          </li>
        </ul>
      </div>
    </div>
  </div>
  <teleport to="body">
    <EditPost v-if="openModal" :toggleModal="toggleModal" :post="post" />
  </teleport>
</template>

<script setup lang="ts">
import { ref, toRefs } from 'vue';
import type { IPostResponse } from '@/api/types';
import { format, parseISO } from 'date-fns';
import EditPost from '@/components/EditPost.vue';
import { useMutation, useQueryClient } from 'vue-query';
import { deletePostFn } from '@/api/postApi';
import { createToast } from 'mosha-vue-toastify';

const VUE_APP_API_URL = 'http://localhost:8000';
const props = defineProps<{
  post: IPostResponse;
}>();

const { post } = toRefs(props);

const queryClient = useQueryClient();
const { isLoading, mutate: deletePost } = useMutation(
  (id: string) => deletePostFn(id),
  {
    onSuccess: (data) => {
      createToast('Post updated successfully', {
        position: 'top-right',
      });
      queryClient.invalidateQueries('posts');
    },
    onError: (error: any) => {
      if (Array.isArray((error as any).response.data.error)) {
        (error as any).response.data.error.forEach((el: any) =>
          createToast(el.message, {
            position: 'top-right',
            type: 'warning',
          })
        );
      } else {
        createToast((error as any).response.data.message, {
          position: 'top-right',
          type: 'danger',
        });
      }
    },
  }
);

function onDelete() {
  if (window.confirm('Are you sure you want to delete this post?')) {
    deletePost(post.value.id);
  }
}

const openSettings = ref(false);
const openModal = ref(false);

function showSettings() {
  openSettings.value = true;
}

function toggleModal() {
  openModal.value = !openModal.value;
  openSettings.value = false;
}
</script>

Vue Query and Axios GET Request

To get JSON data from the server, we use the Vue Query useQuery() hook:


  const {
  data: posts,
  isError,
  isFetching,
  error,
  refetch,
} = useQuery(['posts'], () => getAllPostsFn(), {
  select: (data) => {},
  onError: (error: any) => {},
  onSuccess: (data) => {},
  enabled: true,
  retry: 1
});
  • getAllPostsFn (queryFn): a function used by the useQuery() hook to retrieve data.
  • posts (queryKey): a unique key that will be used by Vue Query to manage the query cache.
  • enabled : Defaults to true . If set to false , the query will be disabled from running automatically.
  • retry : If set to a number , failed queries will retry until the failed count reaches that number.
  • onSuccess : a function that will be called anytime the query successfully fetches new data.
  • onError : This function will be evoked if the query encounters an error.
  • select : This function allows you to transform the data returned by the query.
  • refetch : A function you can use to manually re-fetch the query.

Visit the official Vue Query documentation to read more about the useQuery() hook.

Before writing the code to retrieve all the posts in the database, let’s create a tailwindCss message component that will be shown when there are no posts in the database.

src/components/Message.vue


<template>
  <div
    class="max-w-3xl mx-auto rounded-lg px-4 py-3 shadow-md bg-teal-100 flex items-center justify-center h-40"
    role="alert"
  >
    <span class="text-teal-500 text-xl font-semibold"
      >There are no posts at the moment</span
    >
  </div>
</template>

In addition, let’s create a loading spinner to be displayed when Vue Query is executing the query.

src/components/LoadingProgress.vue


<template>
  <div class="h-screen w-screen fixed">
    <div class="absolute top-64 left-1/2 -translate-x-1/2">
      <Spinner :width="8" :height="8" />
    </div>
  </div>
</template>

<script setup lang="ts">
import Spinner from '@/components/icons/Spinner.vue';
</script>

Now let’s create the React tailwindCss component to get all the posts from the database using Vue Query and Axios.

src/views/HomeView.vue


<template>
  <Header />
  <section class="bg-ct-blue-600 min-h-screen py-12">
    <LoadingProgress v-if="isLoading" />
    <div v-else>
      <Message v-if="posts && posts.length === 0" />
      <div
        v-if="posts && posts.length > 0"
        class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-5 px-6"
      >
        <PostItem v-for="post in posts" :key="post.id" :post="post" />
      </div>
    </div>
  </section>
</template>

<script setup lang="ts">
import { getAllPostsFn } from '@/api/postApi';
import Header from '@/components/Header.vue';
import PostItem from '@/components/PostItem.vue';
import LoadingProgress from '@/components/LoadingProgress.vue';
import Message from '@/components/Message.vue';
import { useQuery } from 'vue-query';

const { data: posts, isLoading } = useQuery(['posts'], () => getAllPostsFn(), {
  select: (data) => data.data.posts,
});
</script>

Conclusion

With this Vue Query, tailwindCss, Vee-validate, and Axios example in Typescript, you’ve learned how to perform the basic CRUD (CREATE, READ, UPDATE, and DELETE) operations against a backend API.

Vue Query and Axios CRUD App Source Code

Check out the complete source code for: