💻 Exemplos: Capítulo 15: Consumo de APIs REST com Axios / ofetch e Interceptors
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 15 — Camada HTTP Desacoplada, Interceptors de JWT e Cancelamento com AbortController
Este exemplo demonstra uma arquitetura corporativa para consumo de APIs REST no Vue 3 utilizando uma instância Axios com interceptores para injeção de token Bearer, renovação automática de refresh token em respostas HTTP 401 e cancelamento de requisições pendentes com AbortController.
1. Cliente HTTP Corporativo: src/services/apiClient.ts
import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios';
export const apiClient: AxiosInstance = axios.create({
baseURL: 'https://api.empresa.com.br/v1',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
});
// Interceptor de Requisição: Injeta Token JWT
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('auth_access_token');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Interceptor de Resposta: Trata Erros e Renovação 401
let isRefreshing = false;
let failedQueue: Array<{ resolve: (token: string) => void; reject: (err: any) => void }> = [];
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = localStorage.getItem('auth_refresh_token');
const res = await axios.post('https://api.empresa.com.br/v1/auth/refresh', { refreshToken });
const newAccessToken = res.data.accessToken;
localStorage.setItem('auth_access_token', newAccessToken);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
failedQueue.forEach((prom) => prom.resolve(newAccessToken));
failedQueue = [];
return apiClient(originalRequest);
} catch (refreshErr) {
failedQueue.forEach((prom) => prom.reject(refreshErr));
failedQueue = [];
localStorage.removeItem('auth_access_token');
window.location.href = '/login';
return Promise.reject(refreshErr);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
2. Composable de Consumo com Cancelamento: src/composables/useApiQuery.ts
import { ref, onScopeDispose, type Ref } from 'vue';
import { apiClient } from '../services/apiClient';
interface QueryResult<T> {
data: Ref<T | null>;
isLoading: Ref<boolean>;
error: Ref<string | null>;
execute: () => Promise<void>;
cancel: () => void;
}
export function useApiQuery<T>(url: string): QueryResult<T> {
const data = ref<T | null>(null) as Ref<T | null>;
const isLoading = ref<boolean>(false);
const error = ref<string | null>(null);
let abortController: AbortController | null = null;
const execute = async (): Promise<void> => {
// Cancela requisição em andamento anterior se houver
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
isLoading.value = true;
error.value = null;
try {
const response = await apiClient.get<T>(url, {
signal: abortController.signal
});
data.value = response.data;
} catch (err: any) {
if (err.name === 'CanceledError' || err.code === 'ERR_CANCELED') {
console.log(`[useApiQuery] Requisição para "${url}" foi cancelada pelo usuário.`);
} else {
error.value = err.response?.data?.message || err.message || 'Falha de comunicação.';
}
} finally {
isLoading.value = false;
}
};
const cancel = (): void => {
if (abortController) {
abortController.abort();
}
};
onScopeDispose(() => {
cancel();
});
return { data, isLoading, error, execute, cancel };
}
3. Componente de Visualização: OrderSearchCatalog.vue
<!-- src/components/OrderSearchCatalog.vue -->
<script setup lang="ts">
import { onMounted } from 'vue';
import { useApiQuery } from '../composables/useApiQuery';
interface OrderItem {
id: string;
customer: string;
total: number;
status: 'PENDING' | 'CONFIRMED' | 'SHIPPED';
}
const { data: orders, isLoading, error, execute, cancel } = useApiQuery<OrderItem[]>('/orders');
onMounted(() => {
execute();
});
</script>
<template>
<div class="max-w-xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-4">
<div class="flex items-center justify-between border-b pb-3">
<h3 class="font-bold text-slate-800">Ordens de Serviço (REST API)</h3>
<div class="flex gap-2">
<button
v-if="isLoading"
@click="cancel"
class="text-xs px-3 py-1.5 bg-red-50 text-red-600 rounded-lg font-semibold"
>
Cancelar Pedido HTTP
</button>
<button
:disabled="isLoading"
@click="execute"
class="text-xs px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-semibold disabled:opacity-50"
>
{{ isLoading ? 'Buscando...' : 'Atualizar Dados' }}
</button>
</div>
</div>
<div v-if="isLoading" class="p-8 text-center text-xs text-slate-500 space-y-2">
<div class="w-6 h-6 border-2 border-indigo-600 border-t-transparent rounded-full animate-spin mx-auto"></div>
<p>Consultando gateway corporativo com interceptor JWT...</p>
</div>
<div v-else-if="error" class="p-4 bg-red-50 border border-red-200 text-red-700 text-xs rounded-xl">
⚠️ Erro retornado pela API: {{ error }}
</div>
<div v-else-if="orders && orders.length > 0" class="divide-y border rounded-xl overflow-hidden text-xs">
<div v-for="order in orders" :key="order.id" class="p-3 flex justify-between items-center hover:bg-slate-50">
<div>
<span class="font-bold text-slate-800">{{ order.customer }}</span>
<p class="text-slate-400 font-mono text-[11px]">{{ order.id }}</p>
</div>
<div class="text-right">
<div class="font-bold text-slate-900">R$ {{ order.total.toFixed(2) }}</div>
<span class="px-2 py-0.5 rounded text-[10px] font-semibold bg-slate-100 text-slate-700">
{{ order.status }}
</span>
</div>
</div>
</div>
</div>
</template>
🖥️ Saída / Comportamento Esperado
Ciclo de Execução:
1. onMounted dispara execute():
- Interceptor injeta cabeçalho "Authorization: Bearer <jwt_access_token>".
- Enquanto aguarda resposta, exibe spinner animado.
2. Cenário de Token Expirado (HTTP 401):
- Response interceptor captura status 401.
- Enfileira requisições simultâneas em 'failedQueue'.
- Dispara chamada atômica para '/auth/refresh'.
- Atualiza token no localStorage e reexecuta todas as requisições originais de forma transparente ao usuário.
3. Cancelamento pelo Usuário:
- Se o usuário clicar em "Cancelar Pedido HTTP" antes do timeout:
- abortController.abort() é disparado.
- Axios rejeita com ERR_CANCELED sem poluir o estado de 'error'.
🔍 Destaques da Implementação
- Fila de Concorrência para Refresh Token: Impede rajada de requisições de renovação simultâneas quando múltiplos componentes disparam queries ao mesmo tempo.
AbortControllerIntegrado: Cancela requisições ativas se o componente for desmontado antes do retorno (onScopeDispose).- Desacoplamento em Camadas: O componente Vue apenas consome os dados e estados (
isLoading,error) sem conhecimento de URLs ou headers.