Neste workshop vamos implementar juntos a integração real entre o frontend React e o backend Spring Boot.
Antes de codar, é importante entender o que já existe no projeto:
import axios from 'axios';
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080',
});O axios é uma biblioteca para fazer requisições HTTP. Em vez de usarmos fetch diretamente, criamos uma instância api já configurada com a URL base do backend. Assim, todas as chamadas partem de http://localhost:8080.
Este arquivo centraliza todas as chamadas ao backend. Cada função representa uma rota da API.
O TanStack Query gerencia o estado assíncrono das requisições:
useQuery— para requisições de leitura (GET). Chama a função automaticamente, guarda o resultado e expõeisLoading.useMutation— para requisições de escrita (POST, PUT, DELETE). Chama a função sob demanda e expõeisPending.
Abra o arquivo src/lib/api.ts. Há três funções com TODO que precisam ser implementadas:
// TODO 1 — buscar todos os lugares
export async function fetchPlaces(): Promise<Place[]> { ... }
// TODO 2 — buscar um lugar pelo ID
export async function fetchPlace(id: string): Promise<Place> { ... }
// TODO 3 — cadastrar um novo lugar
export async function createPlace(data: PlaceFormData, image: File | null): Promise<Place> { ... }GET http://localhost:8080/places
Resposta esperada: array de objetos Place.
export async function fetchPlaces(): Promise<Place[]> {
const { data } = await api.get<Place[]>('/places');
return data;
}| Trecho | Explicação |
|---|---|
api.get('/places') |
Faz uma requisição GET para http://localhost:8080/places |
<Place[]> |
Informa ao TypeScript o tipo da resposta esperada |
const { data } |
Desestrutura o objeto de resposta do axios — data contém o corpo da resposta |
return data |
Retorna o array de lugares para quem chamar a função |
Em src/pages/Home/index.tsx:
const { data: places = [], isLoading } = useQuery({
queryKey: ['places'],
queryFn: fetchPlaces,
});Após implementar, volte ao navegador — os cards aparecerão automaticamente na tela inicial.
GET http://localhost:8080/places/{id}
Resposta esperada: objeto único Place.
export async function fetchPlace(id: string): Promise<Place> {
const { data } = await api.get<Place>(`/places/${id}`);
return data;
}| Trecho | Explicação |
|---|---|
`/places/${id}` |
Template string que monta a URL com o ID dinâmico, ex: /places/123 |
api.get<Place>(...) |
GET que retorna um único objeto no lugar de um array |
const { data } / return |
Mesmo padrão do TODO 1 |
Em src/pages/PlaceDetail/index.tsx e src/pages/EditPlace/index.tsx:
const { data: place } = useQuery({
queryKey: ['places', id],
queryFn: () => fetchPlace(id!),
enabled: !!id,
});Após implementar, as páginas de detalhe e edição passarão a carregar os dados reais.
POST http://localhost:8080/places
Content-Type: multipart/form-data
Este endpoint recebe os dados do lugar e, opcionalmente, um arquivo de imagem. Por isso usamos FormData em vez de JSON.
FormData é um formato nativo do navegador para enviar dados em pares chave-valor, incluindo arquivos. A função buildFormData (já implementada no topo do arquivo) monta esse objeto a partir dos campos do formulário.
export async function createPlace(data: PlaceFormData, image: File | null): Promise<Place> {
const formData = buildFormData(data, image);
const { data: place } = await api.post<Place>('/places', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return place;
}| Trecho | Explicação |
|---|---|
buildFormData(data, image) |
Converte os dados do formulário em um FormData com os campos e a imagem (se houver) |
api.post('/places', formData, ...) |
Faz uma requisição POST enviando o FormData como corpo |
headers: { 'Content-Type': ... } |
Informa ao servidor que o corpo é multipart/form-data (necessário para envio de arquivos) |
const { data: place } |
Renomeia data para place para evitar conflito com o parâmetro data da função |
return place |
Retorna o objeto do lugar recém-criado |
Em src/components/PlaceForm/index.tsx:
const mutation = useMutation({
mutationFn: () => createPlace(formData, imageFile),
onSuccess: () => {
toast.success('Lugar cadastrado com sucesso!');
navigate('/');
},
});Após implementar, o formulário de cadastro enviará os dados para o backend e redirecionará para a home ao concluir.
Leitura (GET) → api.get<Tipo>(url)
Leitura por ID (GET) → api.get<Tipo>(`/rota/${id}`)
Criação (POST) → api.post<Tipo>(url, corpo, { headers })
Com esses três padrões dominados, as demais funções do arquivo (updatePlace, deletePlace, toggleFavorite) seguem a mesma lógica — só mudam o método HTTP e a URL.
Antes de codar, é importante entender o que já existe no projeto:
import axios from 'axios';
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080',
});O axios é uma biblioteca para fazer requisições HTTP. Em vez de usarmos fetch diretamente, criamos uma instância api já configurada com a URL base do backend. Assim, todas as chamadas partem de http://localhost:8080.
Este arquivo centraliza todas as chamadas ao backend. Cada função representa uma rota da API.
const { data: places = [], isLoading } = useQuery({
queryKey: ['places'],
queryFn: fetchPlaces,
});O TanStack Query (useQuery) gerencia o estado assíncrono: ele chama fetchPlaces, guarda o resultado em data, e expõe isLoading enquanto aguarda a resposta.
Abra o arquivo src/lib/api.ts e localize o TODO:
// TODO: implementar a busca de todos os lugares no backend
export async function fetchPlaces(): Promise<Place[]> {
return [];
}Atualmente a função retorna um array vazio. Nosso objetivo é fazer ela buscar os lugares reais do backend.
O backend expõe a seguinte rota:
GET http://localhost:8080/places
Resposta esperada (array de objetos Place):
[
{
"id": "...",
"name": "Café Sereno",
"category": "COFFEE_SHOP",
"city": "São Paulo",
"rating": 4.5,
...
}
]Você pode testar essa rota diretamente no navegador ou em ferramentas como Postman / Insomnia.
Dentro de fetchPlaces, use a instância api para fazer um GET:
export async function fetchPlaces(): Promise<Place[]> {
const { data } = await api.get<Place[]>('/places');
return data;
}O que está acontecendo aqui:
| Trecho | Explicação |
|---|---|
api.get('/places') |
Faz uma requisição GET para http://localhost:8080/places |
<Place[]> |
Informa ao TypeScript o tipo da resposta esperada |
const { data } |
Desestrutura o objeto de resposta do axios — data contém o corpo da resposta |
return data |
Retorna o array de lugares para quem chamar a função |
Com o backend rodando, volte ao navegador e acesse a página inicial. Os cards dos lugares devem aparecer automaticamente — sem nenhuma outra alteração no código, pois o useQuery já está configurado para chamar fetchPlaces e exibir o resultado.
fetchPlaces() → faz GET /places
↓
api.get<Place[]>(...) → axios envia a requisição HTTP
↓
{ data } → axios retorna { data, status, headers, ... }
↓
return data → retorna o array de Place[]
↓
useQuery → recebe o array e atualiza a tela
Agora que você entendeu o padrão, as demais funções no arquivo api.ts seguem a mesma lógica — só mudam o método HTTP (post, put, patch, delete) e a URL. Explore!