Vuetify 3 TypeScript Tutorial Series -Part 3

Habibi Coding | حبيبي كودنق
Nerd For Tech
Published in
6 min readJan 28, 2024
tutorial banner

In Part 2 of this tutorial series, we covered the following steps:

  • Setup Vue CLI
  • Created a new Vuetify 3 project and edited small things

If you missed Part 2, you can find it here: Part 2

Continue adapting the Vue app

Let us now adopt the favicon and logo. Replace the current favicon with the one from my repo public -> favicon.ico : https://raw.githubusercontent.com/habibicoding/task-app-vuetify-medium/part-three/public/favicon.ico

Then replace the current logo with the one from my repo: src -> assets -> logo.png:

logo.svg:

Next open index.html and change the title:

<title>Task App</title>

Open your web browser again on localhost:3000

web browser

The HTTP client for the API

Take a look at the endpoints on your API deployed on render.com

https://{your-project-name}.onrender.com/api/swagger-ui/index.html
endpoints

These are our endpoints, so we have to use an HTTP client to call them.

You can click on each endpoint to see how the request body must look like and what the response body will be

POST request

Create a new folder in src called services create inside a new file called taskApi.ts :

Here we define our base URL and create an Axios instance:

const baseURL = 'https://{your-project-name}/api/v1/';
const api: AxiosInstance = axios.create({baseURL});

Before we continue in src create another directory called constants and here we need a file appConstants.ts . Let us starts with constants for HTTP statuses:

export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
NO_CONTENT: 204,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
};
HTTP_STATUS

Since neither TypeScript nor Axios have default HTTP constants, we have to create our own and I just took the most common one. Most of them we will need later.

Then hop back to taskApi.ts so we can continue with the implementation:

api.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
if (error.response) {
const {status} = error.response;
if (status === HTTP_STATUS.BAD_REQUEST) {
console.error("Bad Request: ", error.response.data);
} else if (status === HTTP_STATUS.INTERNAL_SERVER_ERROR) {
console.error("Internal Server Error: ", error.response.data);
}
} else {
console.error("Error: ", error.message);
}
return Promise.reject(error);
}
);
  1. Success Handler: Returns the response as-is for successful requests.
  2. Error Handler: Checks if there’s a response from the server. Logs specific errors based on the status code (400 for Bad Request, 500 for Internal Server Error). If no server response, logs a generic error.
  3. Error Propagation: Forwards the error for individual request handling with Promise.reject(error).

In summary, this interceptor setup allows centralized logging and handling of errors for all Axios requests made using the api instance. It specifically handles different types of HTTP errors by logging relevant information while allowing the errors to be caught and managed individually in the components or services making the requests.

In srccreate another directory called dtos and create a file called taskDtos.ts :

export enum Priority {
LOW,
MEDIUM,
HIGH
}

export enum TaskState {
OPEN,
CLOSED,
}

export interface TaskCreateRequest {
description: string,
isReminderSet: boolean,
isTaskOpen: boolean,
priority: Priority
}

export interface TaskFetchResponse {
id: number,
description: string,
isReminderSet: boolean | null,
isTaskOpen: boolean | null,
createdOn: string | null,
priority: Priority | null
}

export interface TaskUpdateRequest {
description: string | null,
isReminderSet: boolean | null,
isTaskOpen: boolean | null,
priority: Priority | null
}
  1. Priority: This is an enum typed used by all dtos (data transfer objects).
  2. TaskState: Is needed later for querying from the GET endpoint all open or closed tasks.
  3. The other interfaces are needed for POST, PATCH and GET endpoint.

Then jump back to taskApi.ts and insert the task service interface with the methods to call the endpoints:

interface TaskService {
getTasks: (status: string) => Promise<AxiosResponse>;
createTask: (data: TaskCreateRequest) => Promise<AxiosResponse>;
deleteTask: (id: number) => Promise<AxiosResponse>;
updateTask: (id: number, data: TaskUpdateRequest) => Promise<AxiosResponse>;
}

Lastly we can implement the interface by using the Axios instance:

export const taskService: TaskService = {
getTasks(status: string) {
return api.get(`/tasks?status=${status}`);
},
createTask(data: TaskCreateRequest) {
return api.post('/tasks', data)
},
deleteTask(id: number) {
return api.delete(`/tasks/${id}`)
},
updateTask(id: number, data: TaskUpdateRequest) {
return api.patch(`/tasks/${id}`, data)
}
};

Composables

Next, a method that handles the network request to fetch tasks but also modifies the data for UI changes.

In srccreate another directory called composables and create a file called getTasks.ts , here we need again an interface to declare the fetch state:

interface TasksFetchState {
fetchTasks: (taskType: string) => Promise<void>;
tasks: Ref<TaskFetchResponse[]> | never[];
isLoading: Ref<boolean>;
isNetworkError: Ref<boolean>;
axiosError: Ref<AxiosError | null>;
}

This interface, TasksFetchState, describes the structure for managing the state of fetching tasks:

  • fetchTasks: A function that takes a taskType string and returns a promise. It’s used for fetching tasks asynchronously.
  • tasks: A reactive reference (Vue Ref) to an array of TaskFetchResponse objects. It holds the fetched tasks.
  • isLoading: A reactive boolean indicating if the task fetching process is ongoing.
  • isNetworkError: A reactive boolean that turns true if there is a network error during the task fetching process.
  • axiosError: A reactive reference to an AxiosError object or null, storing any error that occurs during the fetch process.

Before we continue with the implementation in getTasks.ts let us create another file in composables to track request errors, so create the file logRequestError.ts :

import {AxiosError} from "axios";

export default function logRequestError(methodName: string, err: AxiosError | unknown): void {
console.error(`Error in method ${methodName}: ${err instanceof Error ? err.message : String(err)}`);
}

Then jump back to getTasks.ts and add the wrapper function to send the request and adapt the UI according to the response:

export function getTasks(): TasksFetchState {
const tasks = reactive([]);
const isLoading = ref(false);
const isNetworkError = ref(false);
const axiosError = ref(null);

async function fetchTasks(taskType: string): Promise<void> {
isLoading.value = true;
tasks.length = 0;
try {
const response = await taskService.getTasks(taskType);
tasks.splice(0, tasks.length, ...response.data);
isNetworkError.value = false;
} catch (err: AxiosError | unknown) {
logRequestError('fetchTasks', err);
axiosError.value = err instanceof AxiosError ? err : undefined;
isNetworkError.value = true;
} finally {
isLoading.value = false;
}
}

return {fetchTasks, tasks, isLoading, isNetworkError, axiosError};
}

This function, getTasks, sets up and returns a state for fetching tasks:

  • Initializes reactive state variables: tasks (empty array), isLoading (boolean), isNetworkError (boolean), and axiosError (null).
  • Defines fetchTasks, an asynchronous function that:

— Sets isLoading to true and clears the tasksarray.
— Tries to fetch tasks using taskService.getTasks(taskType).
— On success, updates tasks with the response data and sets isNetworkError to false.
— On failure, logs the error, sets axiosError if it’s an Axios error, and sets isNetworkError to true.
— Finally, sets isLoading to false regardless of success or failure.
- Returns an object containing fetchTasks, tasks, isLoading, isNetworkError, and axiosError as part of the TasksFetchState interface.

So now we have finished our first composables file and we can start adding the first Vue component in the next part.

With that, we conclude the first part of this tutorial series. If you found it useful and informative, give it a clap. Here is Part 4

Don’t forget to check out the video playlist on YouTube.

Here is the source code on GitHub, check out the branch: part-three

--

--