- Conteúdo teórico e prático: 1h30
- Exercícios práticos: 1h30
Ao final desta aula, o aluno será capaz de:
- Entender requisições HTTP
- Instalar e configurar Axios
- Fazer requisições GET, POST, PUT, DELETE
- Tratar erros e loading
- Integrar APIs reais em Vue
- Trabalhar com CORS e autenticação
API (Application Programming Interface) é um intermediário que permite comunicação entre aplicações.
REST API:
- Usa HTTP (GET, POST, PUT, DELETE)
- Retorna dados em JSON
- Stateless (cada requisição é independente)
- Identificação de recursos via URLs
Exemplo:
GET /api/usuarios → Listar usuários
GET /api/usuarios/1 → Obter usuário com ID 1
POST /api/usuarios → Criar novo usuário
PUT /api/usuarios/1 → Atualizar usuário 1
DELETE /api/usuarios/1 → Deletar usuário 1
// Fetch (nativo)
fetch('/api/usuarios')
.then(r => r.json())
.then(data => console.log(data))
.catch(erro => console.error(erro))
// Axios (melhor)
axios.get('/api/usuarios')
.then(resposta => console.log(resposta.data))
.catch(erro => console.error(erro))Vantagens do Axios:
- Sintaxe mais simples
- Transformação automática de dados
- Interceptors
- Timeout
- Cancelamento de requisições
npm install axiosimport axios from 'axios'
const app = createApp(App)
// Configurar URL base
axios.defaults.baseURL = 'https://api.exemplo.com'
// Adicionar autenticação em todas as requisições
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
// Disponibilizar globalmente
app.config.globalProperties.$axios = axios<script setup>
import axios from 'axios'
// Usar diretamente
const buscar = async () => {
try {
const resposta = await axios.get('/usuarios')
console.log(resposta.data)
} catch (erro) {
console.error(erro)
}
}
// Ou usar this.$axios (se configurado globalmente)
// const resposta = await this.$axios.get('/usuarios')
</script><template>
<div>
<button @click="buscar">Buscar Usuários</button>
<div v-if="carregando" class="loading">
Carregando...
</div>
<div v-else-if="erro" class="erro">
{{ erro }}
</div>
<ul v-else>
<li v-for="usuario in usuarios" :key="usuario.id">
{{ usuario.nome }} ({{ usuario.email }})
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const usuarios = ref([])
const carregando = ref(false)
const erro = ref(null)
const buscar = async () => {
carregando.value = true
erro.value = null
try {
const resposta = await axios.get('/api/usuarios')
usuarios.value = resposta.data
} catch (e) {
erro.value = 'Erro ao carregar usuários'
console.error(e)
} finally {
carregando.value = false
}
}
// Chamar ao montar componente
onMounted(() => {
buscar()
})
</script>
<style scoped>
.loading, .erro {
padding: 10px;
text-align: center;
}
.erro {
color: red;
background-color: #ffeeee;
}
</style><script setup>
import axios from 'axios'
// Forma 1: String query
const buscar1 = async () => {
await axios.get('/api/usuarios?page=1&limit=10')
}
// Forma 2: Objeto params (recomendado)
const buscar2 = async () => {
await axios.get('/api/usuarios', {
params: {
page: 1,
limit: 10,
ordenar: 'nome'
}
})
}
// Forma 3: ID na URL
const buscarPorId = async (id) => {
const resposta = await axios.get(`/api/usuarios/${id}`)
return resposta.data
}
</script><template>
<form @submit.prevent="criar" class="formulario">
<input v-model="form.nome" placeholder="Nome" required />
<input v-model="form.email" placeholder="Email" required />
<button type="submit" :disabled="enviando">
{{ enviando ? 'Enviando...' : 'Criar' }}
</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const form = ref({
nome: '',
email: ''
})
const enviando = ref(false)
const criar = async () => {
enviando.value = true
try {
const resposta = await axios.post('/api/usuarios', {
nome: form.value.nome,
email: form.value.email
})
console.log('Criado:', resposta.data)
alert('Usuário criado com sucesso!')
// Limpar form
form.value.nome = ''
form.value.email = ''
} catch (erro) {
alert('Erro ao criar usuário')
console.error(erro)
} finally {
enviando.value = false
}
}
</script><script setup>
import axios from 'axios'
const atualizar = async (id, dados) => {
try {
const resposta = await axios.put(`/api/usuarios/${id}`, {
nome: dados.nome,
email: dados.email
})
console.log('Atualizado:', resposta.data)
return resposta.data
} catch (erro) {
console.error('Erro ao atualizar:', erro)
throw erro
}
}
// Ou PATCH (atualização parcial)
const atualizarParcial = async (id, dados) => {
const resposta = await axios.patch(`/api/usuarios/${id}`, dados)
return resposta.data
}
</script><template>
<button @click="deletar(usuario.id)" :disabled="deletando">
{{ deletando ? 'Deletando...' : 'Deletar' }}
</button>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const deletando = ref(false)
const deletar = async (id) => {
if (!confirm('Tem certeza que deseja deletar?')) {
return
}
deletando.value = true
try {
await axios.delete(`/api/usuarios/${id}`)
alert('Deletado com sucesso!')
// Recarregar lista
} catch (erro) {
alert('Erro ao deletar')
console.error(erro)
} finally {
deletando.value = false
}
}
</script><script setup>
import axios from 'axios'
const buscar = async () => {
try {
await axios.get('/api/usuarios')
} catch (erro) {
// Erro de resposta (4xx, 5xx)
if (erro.response) {
console.log('Status:', erro.response.status)
console.log('Dados:', erro.response.data)
if (erro.response.status === 401) {
// Não autenticado
} else if (erro.response.status === 403) {
// Não autorizado
} else if (erro.response.status === 404) {
// Não encontrado
}
}
// Erro de requisição (rede)
else if (erro.request) {
console.log('Sem resposta do servidor')
}
// Erro ao configurar requisição
else {
console.log('Erro:', erro.message)
}
}
}
</script>// main.js
import axios from 'axios'
// Interceptor de resposta
axios.interceptors.response.use(
resposta => resposta, // Sucesso
erro => {
// Tratamento global de erros
if (erro.response?.status === 401) {
// Redirecionar para login
router.push('/login')
}
return Promise.reject(erro)
}
)Obter chave em: https://www.themoviedb.org/settings/api
// services/tmdb.js
import axios from 'axios'
const API_KEY = 'sua_chave_aqui'
const BASE_URL = 'https://api.themoviedb.org/3'
const api = axios.create({
baseURL: BASE_URL,
params: {
api_key: API_KEY,
language: 'pt-BR'
}
})
export default {
buscarFilmes: (query) => api.get('/search/movie', { params: { query } }),
obterFilme: (id) => api.get(`/movie/${id}`),
obterPopulares: () => api.get('/movie/popular'),
obterEmCartaz: () => api.get('/movie/now_playing'),
obterProximos: () => api.get('/movie/upcoming')
}<template>
<div class="tmdb-app">
<h1>The Movie Database</h1>
<!-- Busca -->
<div class="busca">
<input
v-model="query"
@keyup.enter="buscar"
placeholder="Buscar filme..."
/>
<button @click="buscar" :disabled="carregando">
{{ carregando ? 'Buscando...' : 'Buscar' }}
</button>
</div>
<!-- Erro -->
<div v-if="erro" class="erro">{{ erro }}</div>
<!-- Carregando -->
<div v-if="carregando" class="carregando">
Carregando...
</div>
<!-- Resultados -->
<div v-else-if="filmes.length > 0" class="filmes-grid">
<div v-for="filme in filmes" :key="filme.id" class="filme-card">
<img
v-if="filme.poster_path"
:src="`https://image.tmdb.org/t/p/w200${filme.poster_path}`"
:alt="filme.title"
/>
<div v-else class="sem-poster">
Sem Poster
</div>
<h3>{{ filme.title }}</h3>
<p class="avaliacao">★ {{ filme.vote_average.toFixed(1) }}</p>
<p class="ano">{{ new Date(filme.release_date).getFullYear() }}</p>
<button @click="verDetalhes(filme.id)">Ver Detalhes</button>
</div>
</div>
<!-- Vazio -->
<div v-else-if="!carregando && query" class="vazio">
Nenhum filme encontrado
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import tmdbService from '../services/tmdb'
const query = ref('')
const filmes = ref([])
const carregando = ref(false)
const erro = ref(null)
const buscar = async () => {
if (!query.value.trim()) return
carregando.value = true
erro.value = null
try {
const resposta = await tmdbService.buscarFilmes(query.value)
filmes.value = resposta.data.results
} catch (e) {
erro.value = 'Erro ao buscar filmes'
console.error(e)
} finally {
carregando.value = false
}
}
const verDetalhes = (id) => {
// Ir para página de detalhes
console.log('Ver detalhes do filme:', id)
}
// Carregar populares ao montar
const carregarPopulares = async () => {
try {
const resposta = await tmdbService.obterPopulares()
filmes.value = resposta.data.results
} catch (e) {
console.error(e)
}
}
onMounted(() => {
carregarPopulares()
})
</script>
<style scoped>
.tmdb-app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.busca {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
.busca input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.busca button {
padding: 10px 20px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.filmes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 20px;
}
.filme-card {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: transform 0.3s;
}
.filme-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.filme-card img {
width: 100%;
height: 225px;
object-fit: cover;
}
.sem-poster {
width: 100%;
height: 225px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
.filme-card h3 {
padding: 10px;
margin: 0;
font-size: 0.9rem;
min-height: 40px;
}
.avaliacao {
padding: 0 10px;
color: #ff9800;
font-weight: bold;
}
.ano {
padding: 0 10px;
color: #999;
font-size: 0.85rem;
}
.filme-card button {
width: calc(100% - 20px);
margin: 10px;
padding: 8px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.erro, .carregando, .vazio {
text-align: center;
padding: 40px 20px;
}
.erro {
color: #ff6b6b;
background-color: #ffeeee;
}
</style><template>
<div v-if="filme" class="filme-detalhes">
<button @click="voltar" class="btn-voltar">← Voltar</button>
<div class="container">
<div class="poster">
<img
v-if="filme.poster_path"
:src="`https://image.tmdb.org/t/p/w300${filme.poster_path}`"
:alt="filme.title"
/>
</div>
<div class="info">
<h1>{{ filme.title }}</h1>
<p v-if="filme.release_date" class="ano">
Ano: {{ new Date(filme.release_date).getFullYear() }}
</p>
<p class="avaliacao">
⭐ Avaliação: {{ filme.vote_average.toFixed(1) }}/10
</p>
<p v-if="filme.runtime" class="duracao">
Duração: {{ filme.runtime }} minutos
</p>
<div v-if="filme.genres" class="generos">
<span v-for="genero in filme.genres" :key="genero.id" class="genero">
{{ genero.name }}
</span>
</div>
<div class="sinopse">
<h3>Sinopse</h3>
<p>{{ filme.overview }}</p>
</div>
<p v-if="filme.budget" class="meta">
Orçamento: R$ {{ (filme.budget * 5).toLocaleString('pt-BR') }}
</p>
<p v-if="filme.revenue" class="meta">
Arrecadação: R$ {{ (filme.revenue * 5).toLocaleString('pt-BR') }}
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import tmdbService from '../services/tmdb'
const route = useRoute()
const router = useRouter()
const filme = ref(null)
const voltar = () => {
router.back()
}
const carregar = async () => {
try {
const resposta = await tmdbService.obterFilme(route.params.id)
filme.value = resposta.data
} catch (e) {
console.error(e)
}
}
onMounted(() => {
carregar()
})
</script>
<style scoped>
.filme-detalhes {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.btn-voltar {
background-color: #42b883;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 20px;
}
.container {
display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
}
.poster img {
width: 100%;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.info h1 {
margin-top: 0;
font-size: 2.5rem;
}
.ano, .avaliacao, .duracao, .meta {
color: #666;
margin: 10px 0;
}
.avaliacao {
font-size: 1.2rem;
color: #ff9800;
}
.generos {
margin: 20px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.genero {
background-color: #42b883;
color: white;
padding: 5px 12px;
border-radius: 20px;
font-size: 0.9rem;
}
.sinopse {
margin-top: 30px;
}
.sinopse p {
line-height: 1.8;
color: #333;
}
</style>- JSONPlaceholder: API fake para testes
- OpenWeather: Dados climáticos
- RapidAPI: Marketplace de APIs
- GitHub API: Dados de repositórios
- Spotify API: Dados de músicas
- Pokémon API: Dados de Pokémon
- Rick and Morty API: Personagens
- ViaCEP: CEP Brasil
Usar OpenWeather API para:
- Buscar clima por cidade
- Mostrar temp, umidade, descrição
- Adicionar múltiplas cidades
- Salvar favoritos
Usar GitHub API para:
- Buscar usuários/repositórios
- Exibir informações detalhadas
- Listar commits
- Mostrar estatísticas
Usar Pokémon API para:
- Listar pokémon
- Detalhes de cada um
- Filtrar por tipo
- Evoluções
Criar app que:
- Se integre com backend próprio
- CRUD completo
- Autenticação
- Persistência
Usar NewsAPI para:
- Buscar notícias por fonte
- Filtrar por categoria
- Paginação
- Detalhes da notícia
- Entendo o conceito de API REST
- Consigo instalar e configurar Axios
- Consigo fazer requisições GET
- Consigo fazer requisições POST
- Consigo fazer requisições PUT/DELETE
- Consigo tratar erros apropriadamente
- Consigo usar Axios com loading states
- Consigo integrar APIs reais
- Consigo usar interceptors
- Completei todos os 5 exercícios propostos
Nesta aula aprendemos a integrar APIs externas em aplicações Vue.js usando Axios. Os conceitos abordados permitem criar aplicações modernas que se comunicam com servidores e APIs públicas.
Próximos passos:
- Aprender autenticação JWT
- Backend com Node.js/Express
- Deployment de aplicações
- Testes em aplicações Vue
- Performance e otimização
Para 120 horas distribuídas em 3 trimestres:
- Aula 01: Introdução (4h prática)
- Aula 02: Templates (4h prática)
- Aula 03: Reatividade (4h prática)
- Aula 04: Listas (4h prática)
- Exercícios práticos: 20h
- Aula 05: Componentes (4h prática)
- Aula 06: Router (4h prática)
- Aula 07: Pinia (4h prática)
- Aula 08: Formulários (4h prática)
- Exercícios práticos: 20h
- Aula 09: CSS Frameworks (4h prática)
- Aula 10: APIs (4h prática)
- Projetos finais integradores: 32h
Total: 120 horas de dedicação com 60% em exercícios práticos e projetos.