Skip to content

Latest commit

 

History

History
889 lines (717 loc) · 17.4 KB

File metadata and controls

889 lines (717 loc) · 17.4 KB

Aula 10 - APIs e Axios em Vue.js

Duração: 3 horas

  • Conteúdo teórico e prático: 1h30
  • Exercícios práticos: 1h30

Objetivos da Aula

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

1. Introdução a APIs

1.1 O que é uma API?

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

1.2 Axios vs Fetch

// 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

2. Instalação e Configuração do Axios

2.1 Instalar Axios

npm install axios

2.2 Configuração Básica em main.js

import 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

2.3 Usar em Componentes

<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>

3. Requisições HTTP

3.1 GET - Obter Dados

<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>

3.2 GET com Parâmetros

<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>

3.3 POST - Criar Dados

<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>

3.4 PUT - Atualizar Dados

<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>

3.5 DELETE - Deletar Dados

<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>

4. Tratamento de Erros

4.1 Erros Comuns

<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>

4.2 Interceptors para Tratamento Global

// 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)
  }
)

5. Exemplo Prático: The Movie Database (TMDB)

5.1 Configuração

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')
}

5.2 Componente de Busca

<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>

5.3 Componente de Detalhes

<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>

6. Outras APIs Recomendadas

  • 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

7. Referências


8. Exercícios Propostos

Exercício 1: Aplicação de Clima

Usar OpenWeather API para:

  • Buscar clima por cidade
  • Mostrar temp, umidade, descrição
  • Adicionar múltiplas cidades
  • Salvar favoritos

Exercício 2: Visualizador GitHub

Usar GitHub API para:

  • Buscar usuários/repositórios
  • Exibir informações detalhadas
  • Listar commits
  • Mostrar estatísticas

Exercício 3: Pokedéx

Usar Pokémon API para:

  • Listar pokémon
  • Detalhes de cada um
  • Filtrar por tipo
  • Evoluções

Exercício 4: Gerenciador de Tarefas (Backend)

Criar app que:

  • Se integre com backend próprio
  • CRUD completo
  • Autenticação
  • Persistência

Exercício 5: Agregador de Notícias

Usar NewsAPI para:

  • Buscar notícias por fonte
  • Filtrar por categoria
  • Paginação
  • Detalhes da notícia

9. Checklist de Aprendizagem

  • 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

10. Conclusão

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

Horários de Aula Recomendados

Para 120 horas distribuídas em 3 trimestres:

1º Trimestre (40h)

  • 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

2º Trimestre (40h)

  • 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

3º Trimestre (40h)

  • 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.