Vuetify 3 TypeScript Tutorial Series -Part 3
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
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
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
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,
};
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);
}
);
- Success Handler: Returns the response as-is for successful requests.
- 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.
- 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 src
create 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
}
- Priority: This is an enum typed used by all dtos (data transfer objects).
- TaskState: Is needed later for querying from the GET endpoint all open or closed tasks.
- 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 src
create 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 ataskType
string and returns a promise. It’s used for fetching tasks asynchronously.tasks
: A reactive reference (VueRef
) to an array ofTaskFetchResponse
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 anAxiosError
object ornull
, 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), andaxiosError
(null). - Defines
fetchTasks
, an asynchronous function that:
— Sets isLoading
to true and clears the tasks
array.
— 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