Vue 3 Typescript example with Axios: Build CRUD App

In this tutorial, I will show you how to build a Vue 3 Typescript example to consume REST APIs with a CRUD application in that, you can display and modify data using Axios and Vue Router.

More Practice:
Vue.js JWT Authentication with Vuex and Vue Router
Vue File Upload example using Axios

Fullstack:
Vue.js + Node.js + Express + MySQL example
Vue.js + Node.js + Express + PostgreSQL example
Vue.js + Node.js + Express + MongoDB example
Vue.js + Spring Boot + MySQL example
Vue.js + Spring Boot + PostgreSQL example
Vue.js + Spring Boot + MongoDB example
Vue.js + Django example


Overview of Vue 3 Typescript example

We will build a Vue 3 Typescript with Axios called Tutorial Application in that:

  • Each Tutorial has id, title, description, published status.
  • We can create, retrieve, update, delete Tutorials.
  • There is a Search bar for finding Tutorials by title.

Here are screenshots of our Vue 3 Typescript CRUD Application.

– Create a Tutorial:

vue-3-typescript-example-crud-app-create-tutorial

– Retrieve all Tutorials:

vue-3-typescript-example-crud-app-retrieve-tutorial

– Click on Edit button to update a Tutorial:

vue-3-typescript-example-crud-app-retrieve-one-tutorial

On this Page, you can:

  • change status to Published using Publish button
  • delete the Tutorial using Delete button
  • update the Tutorial details with Update button

vue-3-typescript-example-crud-app-update-tutorial

– Search Tutorials by title:

vue-3-typescript-example-crud-app-search-tutorial

The introduction above is for Vue 3 Typescript Client with assumption that we have a Server exporting REST APIs:

Methods Urls Actions
POST /api/tutorials create new Tutorial
GET /api/tutorials retrieve all Tutorials
GET /api/tutorials/:id retrieve a Tutorial by :id
PUT /api/tutorials/:id update a Tutorial by :id
DELETE /api/tutorials/:id delete a Tutorial by :id
DELETE /api/tutorials delete all Tutorials
GET /api/tutorials?title=[keyword] find all Tutorials which title contains keyword

You can find step by step to build a Server like this in one of these posts:
Express, Sequelize & MySQL
Express, Sequelize & PostgreSQL
Express, Sequelize & SQL Server
Express & MongoDb
Spring Boot & MySQL
Spring Boot & PostgreSQL
Spring Boot & MongoDB
Spring Boot & SQL Server
Spring Boot & H2
Spring Boot & Cassandra
Spring Boot & Oracle
Django & MySQL
Django & PostgreSQL
Django & MongoDB

All of them can work well with this Vue App.

Vue.js 3 Component Diagram with Axios & Vue Router

vue-3-typescript-example-crud-app-components

– The App component is a container with router-view. It has navbar that links to routes paths.

TutorialsList component gets and displays Tutorials.
TutorialDetails component has form for editing Tutorial’s details based on :id.
AddTutorial component has form for submission new Tutorial.

– These Components call TutorialDataService methods which use axios to make HTTP requests and receive responses.

Technology

  • vue 3
  • typescript 4
  • vue-router 4
  • axios 0.21.1
  • bootstrap 4

Project Structure

vue-3-typescript-example-crud-app-project-structure

Let me explain it briefly.

package.json contains 4 main modules: vue, typescript, vue-router, axios, bootstrap.
types/Tutorial.ts exports Tutorial interface.
– There are 3 components: TutorialsList, TutorialDetails, AddTutorial.
router.ts defines routes for each component.
http-common.ts initializes axios with HTTP base Url and headers.
TutorialDataService has methods for sending HTTP requests to the Apis.
vue.config.js configures port for this Vue Client.

Setup Vue 3 Typescript Project

Open cmd at the folder you want to save Project folder, run command:
vue create vue-3-typescript-example

You will see some options, choose Manually select features.

Next, we choose the features needed for the project. By default, we see Choose Vue versionBabel, and Linter / Formatter already selected.

We need TypeScript for this project, so just use the arrow keys to navigate down to the TypeScript option and hit the spacebar to select it.

vue-3-typescript-example-crud-app-setup-project

Next, we select 3.x (Preview).

For the remaining configurations:
– Use class-style component syntax? No
– Use Babel alongside TypeScript? Yes
– Pick a linter / formatter config: Prettier
– Pick additional lint features: Lint on save
– Where do you prefer placing config files? In dedicated config files
– Save this for future project? No

vue-3-typescript-example-crud-app-setup-project-final

After the process is done. We create new folders and files like the following tree:


public

src

components

AddTutorial.vue

TutorialDetails.vue

TutorialsList.vue

services

TutorialDataService.ts

types

Tutorial.ts

ResponseData.ts

App.vue

main.ts

package.json


Add Bootstrap to Vue 3 Typescript example

Run command: npm install bootstrap jquery popper.js.

Open src/main.ts and import Bootstrap as following-

import { createApp } from 'vue'
import App from './App.vue'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
...

Add Vue Router to Vue 3 Typescript example

– Run the command: npm install vue-router@4.

– In src folder, create router.ts and define Router as following code:

import { createWebHistory, createRouter } from "vue-router";
import { RouteRecordRaw } from "vue-router";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    alias: "/tutorials",
    name: "tutorials",
    component: () => import("./components/TutorialsList.vue"),
  },
  {
    path: "/tutorials/:id",
    name: "tutorial-details",
    component: () => import("./components/TutorialDetails.vue"),
  },
  {
    path: "/add",
    name: "add",
    component: () => import("./components/AddTutorial.vue"),
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

We create the routes as an array, each route has:

  • path: the URL path where this route can be found.
  • name: optional name to use when we link to this route.
  • component: component to load when this route is called.

We also use createWebHistory to switch from using hash to history mode inside the browser, using the HTML5 history API.

– Open src/main.ts and import the router in our application:

...
import router from "@/router";

createApp(App).use(router).mount("#app");

Property ‘$router’ does not exist

In shims-vue.d.ts, adjust your shim by adding following line like this:

import VueRouter, { Route } from 'vue-router'

/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

declare module 'vue/types/vue' {
  interface Vue {
    $router: VueRouter
  }
}

Now Vue will have a property of $router and Typescript won’t complain.

Add Navbar and Router View to Vue 3 Typescript example

Let’s open src/App.vue, this App component is the root container for our application, it will contain a navbar.

<template>
  <div id="app">
    <nav class="navbar navbar-expand navbar-dark bg-dark">
      <router-link to="/" class="navbar-brand">bezKoder</router-link>
      <div class="navbar-nav mr-auto">
        <li class="nav-item">
          <router-link to="/tutorials" class="nav-link">Tutorials</router-link>
        </li>
        <li class="nav-item">
          <router-link to="/add" class="nav-link">Add</router-link>
        </li>
      </div>
    </nav>

    <div class="container mt-3">
      <router-view />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "App",
});
</script>

Initialize Axios for Vue 3 Typescript HTTP Client

Now we’re gonna install axios with command: npm install axios.
Then, under src folder, we create http-common.ts file like this:

import axios, { AxiosInstance } from "axios";

const apiClient: AxiosInstance = axios.create({
  baseURL: "http://localhost:8080/api",
  headers: {
    "Content-type": "application/json",
  },
});

export default apiClient;

Remember to change the baseURL, it depends on REST APIs url that your Server configures.

For more details about ways to use Axios, please visit:
Axios request: Get/Post/Put/Delete example

Create Data Service

Our service will use axios from HTTP client above to send HTTP requests.

services/TutorialDataService.ts

import http from "@/http-common";

class TutorialDataService {
  getAll(): Promise<any> {
    return http.get("/tutorials");
  }

  get(id: any): Promise<any> {
    return http.get(`/tutorials/${id}`);
  }

  create(data: any): Promise<any> {
    return http.post("/tutorials", data);
  }

  update(id: any, data: any): Promise<any> {
    return http.put(`/tutorials/${id}`, data);
  }

  delete(id: any): Promise<any> {
    return http.delete(`/tutorials/${id}`);
  }

  deleteAll(): Promise<any> {
    return http.delete(`/tutorials`);
  }

  findByTitle(title: string): Promise<any> {
    return http.get(`/tutorials?title=${title}`);
  }
}

export default new TutorialDataService();

Create Tutorial interface

Open types/Tutorial.ts and define Tutorial interface:

export default interface Tutorial {
  id: null;
  title: string;
  description: string;
  published: boolean;
}

Create Response Data interface

Open types/ResponseData.ts and define ResponseData interface:

export default interface ResponseData {
  data: any;
}

Create Vue 3 Typescript Components

We want to use TypeScript, so we need to configure that by assigning ts to lang attribute of the <script> tag.

<script lang="ts">

We typically define components by writing:

export default { ... }

Now we’re gonna use a helper method from Vue:

import { defineComponent } from 'vue'

Then we use it by passing the object we export inside of the defineComponent() function:

export default defineComponent({ ... })`

As I’ve said before, we have 3 components corresponding to 3 routes defined in Vue Router.

Add item Component

This component has a Form to submit new Tutorial with 2 fields: title & description. It calls TutorialDataService.create() method.

components/AddTutorial.vue

<template>
  <div class="submit-form">
    <div v-if="!submitted">
      <div class="form-group">
        <label for="title">Title</label>
        <input
          type="text"
          class="form-control"
          id="title"
          required
          v-model="tutorial.title"
          name="title"
        />
      </div>

      <div class="form-group">
        <label for="description">Description</label>
        <input
          class="form-control"
          id="description"
          required
          v-model="tutorial.description"
          name="description"
        />
      </div>

      <button @click="saveTutorial" class="btn btn-success">Submit</button>
    </div>

    <div v-else>
      <h4>You submitted successfully!</h4>
      <button class="btn btn-success" @click="newTutorial">Add</button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import TutorialDataService from "@/services/TutorialDataService";
import Tutorial from "@/types/Tutorial";
import ResponseData from "@/types/ResponseData";

export default defineComponent({
  name: "add-tutorial",
  data() {
    return {
      tutorial: {
        id: null,
        title: "",
        description: "",
        published: false,
      } as Tutorial,
      submitted: false,
    };
  },
  methods: {
    saveTutorial() {
      let data = {
        title: this.tutorial.title,
        description: this.tutorial.description,
      };

      TutorialDataService.create(data)
        .then((response: ResponseData) => {
          this.tutorial.id = response.data.id;
          console.log(response.data);
          this.submitted = true;
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },

    newTutorial() {
      this.submitted = false;
      this.tutorial = {} as Tutorial;
    },
  },
});
</script>

<style>
.submit-form {
  max-width: 300px;
  margin: auto;
}
</style>

List of items Component

This component calls 3 TutorialDataService methods:

  • getAll()
  • deleteAll()
  • findByTitle()

components/TutorialsList.vue

<template>
  <div class="list row">
    <div class="col-md-8">
      <div class="input-group mb-3">
        <input
          type="text"
          class="form-control"
          placeholder="Search by title"
          v-model="title"
        />
        <div class="input-group-append">
          <button
            class="btn btn-outline-secondary"
            type="button"
            @click="searchTitle"
          >
            Search
          </button>
        </div>
      </div>
    </div>
    <div class="col-md-6">
      <h4>Tutorials List</h4>
      <ul class="list-group">
        <li
          class="list-group-item"
          :class="{ active: index == currentIndex }"
          v-for="(tutorial, index) in tutorials"
          :key="index"
          @click="setActiveTutorial(tutorial, index)"
        >
          {{ tutorial.title }}
        </li>
      </ul>

      <button class="m-3 btn btn-sm btn-danger" @click="removeAllTutorials">
        Remove All
      </button>
    </div>
    <div class="col-md-6">
      <div v-if="currentTutorial.id">
        <h4>Tutorial</h4>
        <div>
          <label><strong>Title:</strong></label> {{ currentTutorial.title }}
        </div>
        <div>
          <label><strong>Description:</strong></label>
          {{ currentTutorial.description }}
        </div>
        <div>
          <label><strong>Status:</strong></label>
          {{ currentTutorial.published ? "Published" : "Pending" }}
        </div>

        <router-link
          :to="'/tutorials/' + currentTutorial.id"
          class="badge badge-warning"
          >Edit</router-link
        >
      </div>
      <div v-else>
        <br />
        <p>Please click on a Tutorial...</p>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import TutorialDataService from "@/services/TutorialDataService";
import Tutorial from "@/types/Tutorial";
import ResponseData from "@/types/ResponseData";

export default defineComponent({
  name: "tutorials-list",
  data() {
    return {
      tutorials: [] as Tutorial[],
      currentTutorial: {} as Tutorial,
      currentIndex: -1,
      title: "",
    };
  },
  methods: {
    retrieveTutorials() {
      TutorialDataService.getAll()
        .then((response: ResponseData) => {
          this.tutorials = response.data;
          console.log(response.data);
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },

    refreshList() {
      this.retrieveTutorials();
      this.currentTutorial = {} as Tutorial;
      this.currentIndex = -1;
    },

    setActiveTutorial(tutorial: Tutorial, index = -1) {
      this.currentTutorial = tutorial;
      this.currentIndex = index;
    },

    removeAllTutorials() {
      TutorialDataService.deleteAll()
        .then((response: ResponseData) => {
          console.log(response.data);
          this.refreshList();
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },

    searchTitle() {
      TutorialDataService.findByTitle(this.title)
        .then((response: ResponseData) => {
          this.tutorials = response.data;
          this.setActiveTutorial({} as Tutorial);
          console.log(response.data);
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },
  },
  mounted() {
    this.retrieveTutorials();
  },
});
</script>

<style>
.list {
  text-align: left;
  max-width: 750px;
  margin: auto;
}
</style>

If you click on Edit button of any Tutorial, the app will direct you to Tutorial page with url: /tutorials/:tutorialId.

You can add Pagination to this Component, just follow instruction in the post:
Vue Pagination with Axios and API (Server Side pagination) example

Item details Component

For getting data & update, delete the Tutorial, this component will use 3 TutorialDataService methods:

  • get()
  • update()
  • delete()

components/TutorialDetails.vue

<template>
  <div v-if="currentTutorial.id" class="edit-form">
    <h4>Tutorial</h4>
    <form>
      <div class="form-group">
        <label for="title">Title</label>
        <input
          type="text"
          class="form-control"
          id="title"
          v-model="currentTutorial.title"
        />
      </div>
      <div class="form-group">
        <label for="description">Description</label>
        <input
          type="text"
          class="form-control"
          id="description"
          v-model="currentTutorial.description"
        />
      </div>

      <div class="form-group">
        <label><strong>Status:</strong></label>
        {{ currentTutorial.published ? "Published" : "Pending" }}
      </div>
    </form>

    <button
      class="badge badge-primary mr-2"
      v-if="currentTutorial.published"
      @click="updatePublished(false)"
    >
      UnPublish
    </button>
    <button
      v-else
      class="badge badge-primary mr-2"
      @click="updatePublished(true)"
    >
      Publish
    </button>

    <button class="badge badge-danger mr-2" @click="deleteTutorial">
      Delete
    </button>

    <button type="submit" class="badge badge-success" @click="updateTutorial">
      Update
    </button>
    <p>{{ message }}</p>
  </div>

  <div v-else>
    <br />
    <p>Please click on a Tutorial...</p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import TutorialDataService from "@/services/TutorialDataService";
import Tutorial from "@/types/Tutorial";
import ResponseData from "@/types/ResponseData";

export default defineComponent({
  name: "tutorial",
  data() {
    return {
      currentTutorial: {} as Tutorial,
      message: "",
    };
  },
  methods: {
    getTutorial(id: any) {
      TutorialDataService.get(id)
        .then((response: ResponseData) => {
          this.currentTutorial = response.data;
          console.log(response.data);
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },

    updatePublished(status: boolean) {
      let data = {
        id: this.currentTutorial.id,
        title: this.currentTutorial.title,
        description: this.currentTutorial.description,
        published: status,
      };

      TutorialDataService.update(this.currentTutorial.id, data)
        .then((response: ResponseData) => {
          console.log(response.data);
          this.currentTutorial.published = status;
          this.message = "The status was updated successfully!";
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },

    updateTutorial() {
      TutorialDataService.update(this.currentTutorial.id, this.currentTutorial)
        .then((response: ResponseData) => {
          console.log(response.data);
          this.message = "The tutorial was updated successfully!";
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },

    deleteTutorial() {
      TutorialDataService.delete(this.currentTutorial.id)
        .then((response: ResponseData) => {
          console.log(response.data);
          this.$router.push({ name: "tutorials" });
        })
        .catch((e: Error) => {
          console.log(e);
        });
    },
  },
  mounted() {
    this.message = "";
    this.getTutorial(this.$route.params.id);
  },
});
</script>

<style>
.edit-form {
  max-width: 300px;
  margin: auto;
}
</style>

Configure Port for Vue 3 Typescript example

Because most of HTTP Server use CORS configuration that accepts resource sharing restricted to some sites or ports, so we also need to configure port for our App.

In project root folder, create vue.config.js file with following content:

module.exports = {
  devServer: {
    port: 8081
  }
}

We’ve set our app running at port 8081.

Run Vue.js 3 Typescript example

You can run our App with command: npm run serve.
If the process is successful, open Browser with Url: http://localhost:8081/ and check it.

This Vue Client will work well with following back-end Rest APIs:
Express, Sequelize & MySQL
Express, Sequelize & PostgreSQL
Express, Sequelize & SQL Server
Express & MongoDb
Spring Boot & MySQL
Spring Boot & PostgreSQL
Spring Boot & MongoDB
Spring Boot & SQL Server
Spring Boot & H2
Spring Boot & Cassandra
Spring Boot & Oracle
Django & MySQL
Django & PostgreSQL
Django & MongoDB

Conclusion

Today we’ve built a Vue 3 Typescript example successfully with Axios and Vue Router. Now we can use the CRUD application to consume REST APIs, display and modify data in a clean way. I hope you apply it in your project at ease.

You can add Pagination Component:
Vue Pagination with Axios and API example

vue-pagination-axios-api-bootstrap-vue-page-change

Or Vuetify version: Vuetify data-table example with a CRUD App | v-data-table

vuetify-data-table-example-crud-app-retrieve-all

Happy learning, see you again!

Further Reading

For more details about ways to use Axios, please visit:
Axios request: Get/Post/Put/Delete example

Fullstack CRUD App:

Integration:
Integrate Vue.js with Spring Boot
Integrate Vue App with Node.js Express

Serverless with Firebase:
Vue Firebase Realtime Database CRUD example
Vue Firebase Firestore CRUD example

Source Code

You can find the complete source code for this tutorial on Github.

14 thoughts to “Vue 3 Typescript example with Axios: Build CRUD App”

    1. Hi, it depends on your use case:
      1- type-checking only => interface
      2- and/or implementation details (creating a new instance for example) => class
      You can use class for type-checking and implementation – whereas you cannot with an interface.

      In some cases, they can be used interchangeably because both define the structure of an object.
      If you want to share structural definition amongst classes, you can define an interface, then implement that interface on the classes.

  1. I’m quite confused with `http-common.ts` why is `apiClient`not being used but rather `http` ?

  2. Great tutorial but I think you should add another level of abstraction to your *DataService

    Since all responses will be Promise<AxiosResponse>, you could return directly the Promise response, like so:

    TutorialDataService.ts (you don’t need to annotate, more about it below)

    get(id) {
    return http.get(`/tutorials/${id}`, data).then((response) => response.data);
    }

    This way, you could do:

    TutorialService.get(id).then(tutorial => { this.currentTutorial = tutorial }).catch….;

    instead of

    TutorialService.get(id).then(response => { this.currentTutorial = response.data } ).catch…;
    —-
    Also, don’t use “any” annotation nor annotate every thing. I usually only annotate when I have to, when the interperter cannot auto infer the type. It’s always best to let inference to be automatic, leave it to the interpreter.

  3. Hi bezkoder

    Man, your tutorial is great, just going to jump right through the next 🙂

    I have a issue with the shims-vue.d.ts file.
    ___________________
    import VueRouter, { Route } from ‘vue-router’



    declare module ‘vue/types/vue’ {
    interface Vue {
    $router: VueRouter
    }
    }
    ____________

    When I import the import VueRouter, { Route } from ‘vue-router’, it stops to recognize the
    import App from “./App.vue” and also at the other components it stops to recognize the .vue files.

    To solve this I just updated the code at the file shims-vue.d.ts for this

    /* eslint-disable */
    declare module ‘*.vue’ {
    import type { DefineComponent } from ‘vue’
    const component: DefineComponent
    export default component
    }

    After that, the only think that I got a issue of with the imports using “@/” inside of the vue components, receiving the error: “Cannot find module ‘@/services/TutorialDataService’ or its corresponding type declarations.Vetur(2307)”

    I just updated it for the relative reference and it worked. Like this:
    import BrokerDataService from “../services/BrokerDataService”;

    Thanks 🙂

    1. If you’re going to disable ESLint, you should stop using Typescript alltogether and go back to vanilla javascript that won’t bother about type checking.

      The whole purpose of Typescript is to enforce type checking, thus the name TYPEscript.
      —–
      Sarcasm apart, my suggestion to you is to use Class Style components in Vue, including your App.vue file, when using Typescript. The guide page comes with a Typescript use reference that might benefit you. Just don’t forget to use script lang=”ts”.

Comments are closed to reduce spam. If you have any question, please send me an email.