Let me share with you with this guide a comfortable / easy configuration for Axios in a Vue.js 3 app with Pinia as our state management. The general idea is managing an easy way to add and use different endpoints when you are working with an API REST.
Scope of this guide:
This guide is not a tutorial to learn Pinia or Axios.
All the backend solution is not covered in this guide.
Sections:
- Project setup
- Install Axios and Pinia. Axios and Pinia configuration
- Separate the services in our Vue.js app
- Integrate the services with the Pinia stores
- Finishing the services
- Conclusion
1. Project setup
As a default Vuejs 3 app now in these days, Vite is the way to go. Here are the commands. I will use yarn.
npm create vite@latest
or
yarn create vite
After run one of the above commands, select Vue
Then, select JavaScript or TypeScript. I will use TypeScript
Finally, go to the new project created (folder) and install the dependencies with "npm install" or "yarn".
After the installation, create an .env file in the root folder of your project to configure some special variables, for example.
//.env file
VITE_API_ENDPOINT=http://localhost:8080/api/v1/
This variable will be the URL for our backend API
2. Installing Axios and Pinia. Axios and Pinia configuration
Axios is useful to handle http request and Pinia is our global state management
#Installing dependencies
yarn add axios pinia
or
npm install axios pinia
2.a Pinia configuration
To use Pinia in all your app, first we need to create the "root Store" that is a Pinia instance that we need to pass it in our app. We have to add it as a plugin in the main.ts file
// main.ts file
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
createApp(App).use(pinia).mount("#app");
2.b Axios configuration
We will configure Axios in a way we can use a common URL for our api and from there, we will add all the new endpoints that we are going to use for the project.
First, create a "services" folder inside the "src" folder in your app. It is called "services" as a simple convention. Then we will create two files inside the "services" folder. The "api.ts" and "types.ts" files.
The "api.ts" file will be useful to configure our axios instance
The "types.ts" file will be useful to define our main types
// api.ts file
import axios from 'axios';
const instance = axios.create({
baseURL: import.meta.env.VITE_API_ENDPOINT,
});
export default instance
In this "api.ts" file we will have the base configuration for all our endpoints, and we will be able to use the "instance" variable to build all the endpoint that we need for our project. For this guide, this is the minimum configuration that we need.
//types.ts file
export type APIResponse<T> = {
success: boolean
content: T;
status?: number;
}
This type will represent all the responses body for our different endpoints, as you can see is generic due to the fact that our responses will be different depending on the endpoint that we use.
3. Separate the services in our Vue.js app
In this example, the project will be a basic University. We will have:
- Teachers
- Students
- Schools
- Courses
Now, let's create another folder inside our "services" folder call "schools". In this folder we will create two more files, files "types.ts" and "index.ts".
The "types.ts" will be the file for all our types, and the "index.ts" file will be the place where we create the endpoints for the schools.
Our "types.ts" file could look like this
export type School = {
id: number;
name: string;
description: string;
};
export type InputCreateSchool = {
id: number;
name: string;
description: string;
};
export type InputUpdateSchool = {
id: number;
description: string;
};
We have the School type to represent the list of schools. The InputCreateSchool type is the body of our JSON when we have to send this body through a form to the backend, and the InputUpdateSchool type to represent the body of our form when we have to update the school information.
Now in the "index.ts" file we can use our new types like this
// the axios instance and types
import http from "../api";
import { APIResponse } from "../types";
import { School, InputCreateSchool, InputUpdateSchool } from "./types";
async function getSchools() {
return await http.get<APIResponse<School[]>>("school");
}
async function deleteSchool(id: number) {
return await http.delete<APIResponse<boolean>>(`school/${id}`);
}
async function createSchool(input: InputCreateSchool) {
return await http.post<APIResponse<School>>("school", input);
}
async function updateSchool(input: InputUpdateSchool) {
return await http.put<APIResponse<boolean>>("school", input);
}
export default {
getSchools,
createSchool,
updateSchool,
deleteSchool,
};
Perfect,as you can see at the beginning we are importing the axios instance that has our base URL, then we are importing the main type for all our responses and finally the specific types for our "school" endpoint.
The functions represent our CRUD for our "school" endpoint, some of them receives an argument for the http method that they are going to use with its specific input.
Finally, we export these functions to use it in another step of this guide, before we go there, here there is a picture of how is our folder structure so far
Now, let's use the new functions of our new endpoint. To do this, let's create another file in the "services" folder called "index.ts". The main purpose of this file is to be the only place where we can load all our different services.
//services/index.ts
import schoolController from "./schools";
export const API = {
schools: schoolController,
};
As you can see, here we load all the functions from the "schools" folder with the name of "schoolController", I use the "controller" name convention due to Express.js. In Express.js is really common to call controllers to all the functions that we are using for the routes. It's like a habit.
After the importation, we create the constant that we are going to be using to use all the functions for all our endpoints in our project. It's called "API" because it is cool. Here there is a picture with our folder structure so far.
4. Integrate the services with the Pinia stores
In this section, we will be using our recent "API" constant to work with all the endpoints we have so far.
First, let's prepare the place for our Pinia stores. Let's create a folder inside our "src" root folder called "store" as a good convention, then another folder inside the "store" folder called "schools". Inside this last folder, we are going to create a file called "index.ts" to create our store.
With Pinia, there are two ways to create a store, in an object or with a callback. I will use the second options as it's like the Composition Api
// store/schools/index.
import { defineStore } from "pinia";
import { ref } from 'vue';
import { School } from "../../services/schools/types";
export const useSchoolStore = defineStore("schoolStore", () => {
const schools = ref<School[]>([]);
function initSchools(data: School[]) {
schools.value = data;
}
function removeSchool(id: number) {
const idx = schools.value.findIndex(s => s.id === id);
if (idx === -1) return;
schools.value.splice(idx , 1);
}
return {
schools,
initSchools,
removeSchool,
};
});
As you can see in the code, for now we have created the "schools" state to store all our schools and some functions to manipulate this state. And now, how do we get the schools from our backend?.
A clean way to do it is using the concept of vuex's actions. Do you remember the actions in Vuex?
The actions in Vuex allows committing mutations after asynchronous operations. Do not mutate the state directly, this responsibility is for the mutations
Another thing to know is that the vuex's actions are triggered with dispatcher methods, because of that our asynchronous operations will have "dispatch" in the name as a convention.
With the above mentioned, we will create dispatchers for CRUD operations.
Thanks to Typescript and our "API" constant, we have easily autocompletion of our methods for our "school" endpoints
Finally, our store could look something like this.
import { defineStore } from "pinia";
import { ref } from "vue";
import {
InputCreateSchool,
InputUpdateSchool,
School,
} from "../../services/schools/types";
import { APIResponse } from "../../services/types";
import { API } from "../../services";
import { AxiosError } from "axios";
export const useSchoolStore = defineStore("schoolStore", () => {
const schools = ref<School[]>([]);
function initSchools(data: School[]) {
schools.value = data;
}
function addNewSchool(school: School) {
schools.value.push(school);
}
function removeSchool(id: number) {
const idx = schools.value.findIndex((s) => s.id === id);
if (idx === -1) return;
schools.value.splice(idx, 1);
}
async function dispatchGetSchools(): Promise<APIResponse<null>> {
try {
const { status, data } = await API.school.getSchools();
if (status === 200) {
initSchools(data.content);
return {
success: true,
content: null,
};
}
} catch (error) {
const _error = error as AxiosError<string>;
return {
success: false,
status: _error.response?.status,
content: null,
};
}
return {
success: false,
content: null,
status: 400,
};
}
async function dispatchCreateSchool(
input: InputCreateSchool
): Promise<APIResponse<null>> {
try {
const { status, data } = await API.school.createSchool(input);
if (status === 200) {
addNewSchool(data.content);
return {
success: true,
content: null,
};
}
} catch (error) {
const _error = error as AxiosError<string>;
return {
success: false,
status: _error.response?.status,
content: null,
};
}
return {
success: false,
content: null,
status: 400,
};
}
async function dispatchDeleteSchool(id: number): Promise<APIResponse<null>> {
try {
const { status } = await API.school.deleteSchool(id);
if (status === 200) {
removeSchool(id);
return {
success: true,
content: null,
};
}
} catch (error) {
const _error = error as AxiosError<string>;
return {
success: false,
status: _error.response?.status,
content: null,
};
}
return {
success: false,
content: null,
status: 400,
};
}
async function dispatchUpdateSchool(
input: InputUpdateSchool
): Promise<APIResponse<null>> {
try {
const { status } = await API.school.updateSchool(input);
if (status === 200) {
return {
success: true,
content: null,
};
}
} catch (error) {
const _error = error as AxiosError<string>;
return {
success: false,
status: _error.response?.status,
content: null,
};
}
return {
success: false,
content: null,
status: 400,
};
}
return {
schools,
initSchools,
removeSchool,
dispatchGetSchools,
dispatchCreateSchool,
dispatchDeleteSchool,
dispatchUpdateSchool,
};
});
Nice, we have created the CRUD operations with dispatcher functions. As you can see, these dispatchers commits "mutations" after the asynchronous operation is resolved and mutate the state. Notice that each dispatcher returns a promise with a specific type, so we can respect this contract when we use these functions.
The errors are being controlled with a try-catch block, the error's response can be customized as you want. For this guide, this is enough.
Here there is a picture of our store so far.
Now we are ready to use these dispatchers, in a Schools component for example we can do this.
<template>
<div>
<h1>The list of schools</h1>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { useSchoolStore } from "../store/schools";
const schoolStore = useSchoolStore();
onMounted(async () => {
const { success, status } = await schoolStore.dispatchGetSchools();
if (!success) {
alert("Ups, something happened 🙂");
console.log("Api status ->", status);
}
});
</script>
<style scoped></style>
In this case, we are loading our schools when the component is mounted, since the dispatcher returns an object with useful properties, we can use them to handle some messages for our users and so on.
5. Finishing the services
To end this guide, let's create the rest of our services to see our "API" constant in action.
- Courses service
// the index.ts file
import http from "../api";
import { APIResponse } from "../types";
import { Course, InputCreateCourse, InputUpdateCourse } from "./types";
async function getCourses() {
return await http.get<APIResponse<Course[]>>("course");
}
async function deleteCourse(id: number) {
return await http.delete<APIResponse<boolean>>(`course/${id}`);
}
async function createCourse(input: InputCreateCourse) {
return await http.post<APIResponse<Course>>("course", input);
}
async function updateCourse(input: InputUpdateCourse) {
return await http.put<APIResponse<boolean>>("course", input);
}
export default {
getCourses,
createCourse,
updateCourse,
deleteCourse,
};
// the types.ts file
export type Course = {
id: number;
title: string;
description: string;
schoolID: number;
};
export type InputCreateCourse = {
title: string;
description: string;
schoolID: number;
};
export type InputUpdateCourse = {
id: number;
title: string;
description: string;
schoolID: number;
};
- Teachers service
// the index.ts file
import http from "../api";
import { APIResponse } from "../types";
import { Teacher, InputCreateTeacher, InputUpdateTeacher } from "./types";
async function getTeachers() {
return await http.get<APIResponse<Teacher[]>>("teacher");
}
async function deleteTeacher(id: number) {
return await http.delete<APIResponse<boolean>>(`teacher/${id}`);
}
async function createTeacher(input: InputCreateTeacher) {
return await http.post<APIResponse<Teacher>>("teacher", input);
}
async function updateTeacher(input: InputUpdateTeacher) {
return await http.put<APIResponse<boolean>>("teacher", input);
}
export default {
getTeachers,
createTeacher,
updateTeacher,
deleteTeacher,
};
// the types.ts file
export type Teacher = {
id: number;
firstName: string;
lastName: string;
age: number;
schoolID: number;
courseID: number;
};
export type InputCreateTeacher = {
firstName: string;
lastName: string;
age: number;
schoolID: number;
courseID: number;
};
export type InputUpdateTeacher = {
id: number;
firstName: string;
lastName: string;
age: number;
schoolID: number;
courseID: number;
};
- Students service
//the index.ts file
import http from "../api";
import { APIResponse } from "../types";
import { Student, InputCreateStudent, InputUpdateStudent } from "./types";
async function getStudents() {
return await http.get<APIResponse<Student[]>>("student");
}
async function deleteStudent(id: number) {
return await http.delete<APIResponse<boolean>>(`student/${id}`);
}
async function createStudent(input: InputCreateStudent) {
return await http.post<APIResponse<Student>>("student", input);
}
async function updateStudent(input: InputUpdateStudent) {
return await http.put<APIResponse<boolean>>("student", input);
}
export default {
getStudents,
createStudent,
updateStudent,
deleteStudent,
};
//the types.ts file
export type Student = {
id: number;
firstName: string;
lastName: string;
age: number;
schoolID: number;
};
export type InputCreateStudent = {
firstName: string;
lastName: string;
age: number;
schoolID: number;
};
export type InputUpdateStudent = {
id: number;
firstName: string;
lastName: string;
age: number;
schoolID: number;
};
Now, our "services/index.ts" file has more controllers. Take a look:
import schoolController from "./schools";
import courseController from "./courses";
import studentsController from "./students";
import teachersController from "./teachers";
export const API = {
school: schoolController,
course: courseController,
student: studentsController,
teacher: teachersController,
};
Here there is a picture of our services folder so far.
If we check our "API" constant in the last dispatcher, now we have this.
The controllers are in our hand, and each of them has their respective functions, for example.
Now it is up to you to keep creating your stores, it is a good practice indeed.
6. Conclusion
With these abstractions, it's easy to add more and more services
Update any endpoint's URL it's not a problem, you just need to go to a specific file and change it once.
You have a complete control of all your endpoints with a single constant
It's easy to replicate how the dispatchers works for any endpoint you need, even with more complex functionalities.
Top comments (0)