🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Voltar ao Curso
MÓDULO 2
AULA 6

API Client + React Query

Cliente HTTP robusto para consumir a API do Restaurantix com React Query para cache inteligente, sincronização automática e otimizações.

55 min
API Client
React Query
🚀 React Query + API Client

React Query (TanStack Query) é a biblioteca mais poderosa para gerenciar estado do servidor, cache e sincronização de dados.

React Query

  • Cache inteligente: Dados em memória
  • Background updates: Sincronização automática
  • Optimistic updates: UI responsiva
  • Error handling: Retry automático
  • DevTools: Debug avançado

API Client

  • Type-safe: TypeScript completo
  • Interceptors: Auth e error handling
  • Retry logic: Tentativas automáticas
  • Request/Response: Transformações
  • Timeout: Controle de tempo
Instalação e Setup

Instalar dependências

npm install @tanstack/react-query @tanstack/react-query-devtools
npm install axios  # ou pode usar fetch nativo

src/lib/query-client.ts

import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Tempo que os dados ficam "fresh" (não refetch)
      staleTime: 1000 * 60 * 5, // 5 minutos
      
      // Tempo que os dados ficam no cache
      gcTime: 1000 * 60 * 30, // 30 minutos (era cacheTime)
      
      // Retry automático em caso de erro
      retry: (failureCount, error: any) => {
        // Não retry em erros 4xx (client errors)
        if (error?.status >= 400 && error?.status < 500) {
          return false
        }
        // Máximo 3 tentativas para outros erros
        return failureCount < 3
      },
      
      // Refetch quando a janela ganha foco
      refetchOnWindowFocus: false,
      
      // Refetch quando reconecta à internet
      refetchOnReconnect: true,
    },
    mutations: {
      // Retry para mutations
      retry: 1,
    },
  },
})

src/providers/query-provider.tsx

'use client'

import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from '@/lib/query-client'

interface QueryProviderProps {
  children: React.ReactNode
}

export function QueryProvider({ children }: QueryProviderProps) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools 
        initialIsOpen={false} 
        position="bottom-right"
      />
    </QueryClientProvider>
  )
}
API Client Avançado

src/lib/api-client.ts

// ✅ CORRIGIDO: Usando fetch com cookies em vez de axios com tokens
import { env } from './env'

export interface ApiError {
  message: string
  code?: string
  status?: number
  details?: any
}

class ApiClient {
  private baseURL: string

  constructor() {
    this.baseURL = env.NEXT_PUBLIC_API_URL
  }

  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${this.baseURL}${endpoint}`
    
    // ✅ CORREÇÃO PRINCIPAL: credentials: 'include' para cookies
    const config: RequestInit = {
      ...options,
      credentials: 'include', // Envia cookies automaticamente
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
    }

    // Log da requisição em desenvolvimento
    if (process.env.NODE_ENV === 'development') {
      console.log('🚀 API Request:', {
        method: config.method || 'GET',
        url,
        data: config.body,
      })
    }

    try {
      const response = await fetch(url, config)
      
      // Log da resposta em desenvolvimento
      if (process.env.NODE_ENV === 'development') {
        console.log('✅ API Response:', {
          status: response.status,
          url,
        })
      }

      // Se não há conteúdo, retorna undefined
      if (response.status === 204) {
        return undefined as T
      }

      const data = await response.json()

      if (!response.ok) {
        const apiError: ApiError = {
          message: data?.message || 'Erro do servidor',
          code: data?.code,
          status: response.status,
          details: data?.details,
        }

        // Não autenticado - redirecionar para login
        if (response.status === 401) {
          if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
            window.location.href = '/login'
          }
        }

        console.error('❌ API Error:', apiError)
        throw apiError
      }

      return data
    } catch (error) {
      if (error instanceof Error && error.name === 'TypeError') {
        // Erro de rede
        const apiError: ApiError = {
          message: 'Erro de conexão. Verifique sua internet.',
          status: 0,
        }
        console.error('❌ Network Error:', apiError)
        throw apiError
      }
      
      throw error
    }
  }

  // ✅ Métodos HTTP simplificados (sem gerenciamento de token)
  async get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'GET' })
  }

  async post<T>(endpoint: string, data?: any): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: data ? JSON.stringify(data) : undefined,
    })
  }

  async put<T>(endpoint: string, data?: any): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: data ? JSON.stringify(data) : undefined,
    })
  }

  async patch<T>(endpoint: string, data?: any): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'PATCH',
      body: data ? JSON.stringify(data) : undefined,
    })
  }

  async delete<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'DELETE' })
  }
}

export const apiClient = new ApiClient()
Services da API

src/services/auth-service.ts

import { apiClient } from '@/lib/api-client'

export interface User {
  id: string
  name: string
  email: string
  phone: string | null
  role: 'manager' | 'customer'
  createdAt: string
  updatedAt: string
}

export interface Restaurant {
  id: string
  name: string
  description: string | null
  managerId: string
  createdAt: string
  updatedAt: string
}

export interface CsrfTokenResponse {
  token: string
}

export const authService = {
  // ✅ CORRETO: GET para obter token CSRF
  async getCsrfToken(): Promise<CsrfTokenResponse> {
    return apiClient.get('/csrf-token')
  },

  // ✅ CORRETO: POST para enviar magic link
  async sendMagicLink(email: string): Promise<{ message: string }> {
    return apiClient.post('/auth-links/authenticate', { email })
  },

  // ✅ CORRIGIDO: GET (não PATCH) para autenticar
  async authenticateFromLink(code: string, redirect: string): Promise<void> {
    const params = new URLSearchParams({ code, redirect })
    return apiClient.get(`/auth-links/authenticate?${params}`)
  },

  // ✅ NOVO: GET para obter perfil do usuário
  async getProfile(): Promise<User> {
    return apiClient.get('/me')
  },

  // ✅ CORRIGIDO: GET (não POST) para logout
  async signOut(): Promise<{ message: string }> {
    return apiClient.get('/sign-out')
  },
}

src/services/restaurant-service.ts

import { apiClient } from '@/lib/api-client'

export interface CreateRestaurantRequest {
  restaurantName: string
  managerName: string
  email: string
  phone: string
}

export interface CreateRestaurantResponse {
  restaurantId: string
}

export const restaurantService = {
  // ✅ NOVO: POST para criar restaurante
  async createRestaurant(data: CreateRestaurantRequest): Promise<CreateRestaurantResponse> {
    return apiClient.post('/restaurants', data)
  },

  // ✅ NOVO: GET para obter restaurante gerenciado
  async getManagedRestaurant(): Promise<Restaurant> {
    return apiClient.get('/managed-restaurant')
  },
}

src/services/orders-service.ts

import { apiClient } from '@/lib/api-client'
import type { Order, OrderStatus } from '@/types/orders'

export interface GetOrdersParams {
  orderId?: string
  customerName?: string
  status?: OrderStatus
  page?: number
  perPage?: number
}

export interface GetOrdersResponse {
  orders: Order[]
  meta: {
    pageIndex: number
    perPage: number
    totalCount: number
  }
}

export interface OrderDetailsResponse {
  order: Order & {
    orderItems: Array<{
      id: string
      priceInCents: number
      quantity: number
      product: {
        name: string
      }
    }>
    customer: {
      name: string
      email: string
      phone: string | null
    }
  }
}

export const ordersService = {
  async getOrders(params: GetOrdersParams = {}): Promise<GetOrdersResponse> {
    const searchParams = new URLSearchParams()
    
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined && value !== null) {
        searchParams.append(key, String(value))
      }
    })

    return apiClient.get(`/orders?${searchParams}`)
  },

  async getOrderDetails(orderId: string): Promise<OrderDetailsResponse> {
    return apiClient.get(`/orders/${orderId}`)
  },

  async approveOrder(orderId: string): Promise<void> {
    return apiClient.patch(`/orders/${orderId}/approve`)
  },

  async dispatchOrder(orderId: string): Promise<void> {
    return apiClient.patch(`/orders/${orderId}/dispatch`)
  },

  async deliverOrder(orderId: string): Promise<void> {
    return apiClient.patch(`/orders/${orderId}/deliver`)
  },

  async cancelOrder(orderId: string): Promise<void> {
    return apiClient.patch(`/orders/${orderId}/cancel`)
  },
}

src/services/metrics-service.ts

import { apiClient } from '@/lib/api-client'

export interface MonthRevenueResponse {
  receipt: number
  diffFromLastMonth: number
}

export interface DayOrdersAmountResponse {
  amount: number
  diffFromYesterday: number
}

export interface MonthOrdersAmountResponse {
  amount: number
  diffFromLastMonth: number
}

export interface PopularProductsResponse {
  products: Array<{
    product: string
    amount: number
  }>
}

export interface DailyReceiptInPeriodResponse {
  data: Array<{
    date: string
    receipt: number
  }>
}

export const metricsService = {
  // ✅ CORRETO: Receita mensal
  async getMonthRevenue(): Promise<MonthRevenueResponse> {
    return apiClient.get('/metrics/month-revenue')
  },

  // ✅ NOVO: Pedidos do dia (estava faltando)
  async getDayOrdersAmount(): Promise<DayOrdersAmountResponse> {
    return apiClient.get('/metrics/day-orders-amount')
  },

  // ✅ CORRETO: Pedidos do mês
  async getMonthOrdersAmount(): Promise<MonthOrdersAmountResponse> {
    return apiClient.get('/metrics/month-orders-amount')
  },

  // ✅ CORRETO: Produtos populares
  async getPopularProducts(): Promise<PopularProductsResponse> {
    return apiClient.get('/metrics/popular-products')
  },

  // ✅ NOVO: Receitas diárias em período (estava faltando)
  async getDailyReceiptInPeriod(params: {
    from?: string
    to?: string
  } = {}): Promise<DailyReceiptInPeriodResponse> {
    const searchParams = new URLSearchParams()
    
    if (params.from) searchParams.append('from', params.from)
    if (params.to) searchParams.append('to', params.to)

    return apiClient.get(`/metrics/daily-receipt-in-period?${searchParams}`)
  },
}
Hooks com React Query

src/hooks/use-auth-queries.ts

// ✅ CORRIGIDO: Hooks atualizados para usar cookies e endpoints corretos
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { authService } from '@/services/auth-service'
import { restaurantService } from '@/services/restaurant-service'
import { toast } from 'sonner'

// ✅ NOVO: Hook para obter token CSRF
export function useCsrfToken() {
  return useQuery({
    queryKey: ['auth', 'csrf-token'],
    queryFn: authService.getCsrfToken,
    staleTime: 1000 * 60 * 30, // 30 minutos
  })
}

// ✅ CORRIGIDO: Hook para obter perfil do usuário
export function useProfile() {
  return useQuery({
    queryKey: ['auth', 'profile'],
    queryFn: authService.getProfile,
    retry: false, // Não retry em 401
    staleTime: 1000 * 60 * 10, // 10 minutos
  })
}

// ✅ NOVO: Hook para obter restaurante gerenciado
export function useManagedRestaurant() {
  const { data: user } = useProfile()
  
  return useQuery({
    queryKey: ['restaurant', 'managed'],
    queryFn: restaurantService.getManagedRestaurant,
    enabled: !!user && user.role === 'manager',
    retry: false,
    staleTime: 1000 * 60 * 15, // 15 minutos
  })
}

// ✅ CORRIGIDO: Hook para enviar magic link
export function useSendMagicLink() {
  return useMutation({
    mutationFn: authService.sendMagicLink,
    onSuccess: () => {
      toast.success('Link de autenticação enviado para seu email!')
    },
    onError: (error: any) => {
      toast.error(error.message || 'Erro ao enviar link')
    },
  })
}

// ✅ CORRIGIDO: Hook para criar restaurante
export function useCreateRestaurant() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: restaurantService.createRestaurant,
    onSuccess: () => {
      // Invalidar cache do restaurante gerenciado
      queryClient.invalidateQueries({ queryKey: ['restaurant', 'managed'] })
      toast.success('Restaurante criado com sucesso!')
    },
    onError: (error: any) => {
      toast.error(error.message || 'Erro ao criar restaurante')
    },
  })
}

// ✅ CORRIGIDO: Hook para logout
export function useSignOut() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: authService.signOut,
    onSuccess: () => {
      // Limpar todo o cache
      queryClient.clear()
      toast.success('Logout realizado com sucesso!')
      // Redirecionar para login
      window.location.href = '/login'
    },
    onError: (error: any) => {
      toast.error(error.message || 'Erro ao fazer logout')
    },
  })
}

// ✅ NOVO: Hook combinado para autenticação
export function useAuth() {
  const { data: user, isLoading: isLoadingUser } = useProfile()
  const { data: restaurant, isLoading: isLoadingRestaurant } = useManagedRestaurant()
  const { data: csrfToken } = useCsrfToken()
  
  const sendMagicLinkMutation = useSendMagicLink()
  const signOutMutation = useSignOut()
  
  return {
    user,
    restaurant,
    csrfToken,
    isLoading: isLoadingUser || isLoadingRestaurant,
    isAuthenticated: !!user,
    sendMagicLink: sendMagicLinkMutation.mutate,
    signOut: signOutMutation.mutate,
    isLoadingSendMagicLink: sendMagicLinkMutation.isPending,
    isLoadingSignOut: signOutMutation.isPending,
  }
}

src/hooks/use-orders-queries.ts

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { ordersService, type GetOrdersParams } from '@/services/orders-service'
import { useToast } from '@/hooks/use-toast'

export function useOrders(params: GetOrdersParams = {}) {
  return useQuery({
    queryKey: ['orders', params],
    queryFn: () => ordersService.getOrders(params),
    staleTime: 1000 * 60 * 2, // 2 minutos
  })
}

export function useOrderDetails(orderId: string) {
  return useQuery({
    queryKey: ['order', orderId],
    queryFn: () => ordersService.getOrderDetails(orderId),
    enabled: !!orderId,
    staleTime: 1000 * 60 * 5, // 5 minutos
  })
}

export function useApproveOrder() {
  const queryClient = useQueryClient()
  const { toast } = useToast()
  
  return useMutation({
    mutationFn: ordersService.approveOrder,
    onSuccess: () => {
      // Invalidar cache de pedidos
      queryClient.invalidateQueries({ queryKey: ['orders'] })
      
      toast.success('Pedido aprovado!', 'O pedido foi aprovado com sucesso.')
    },
    onError: (error: any) => {
      toast.error('Erro ao aprovar pedido', error.message)
    },
  })
}

export function useDispatchOrder() {
  const queryClient = useQueryClient()
  const { toast } = useToast()
  
  return useMutation({
    mutationFn: ordersService.dispatchOrder,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] })
      toast.success('Pedido despachado!', 'O pedido está a caminho.')
    },
    onError: (error: any) => {
      toast.error('Erro ao despachar pedido', error.message)
    },
  })
}

export function useDeliverOrder() {
  const queryClient = useQueryClient()
  const { toast } = useToast()
  
  return useMutation({
    mutationFn: ordersService.deliverOrder,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] })
      toast.success('Pedido entregue!', 'O pedido foi entregue com sucesso.')
    },
    onError: (error: any) => {
      toast.error('Erro ao entregar pedido', error.message)
    },
  })
}

export function useCancelOrder() {
  const queryClient = useQueryClient()
  const { toast } = useToast()
  
  return useMutation({
    mutationFn: ordersService.cancelOrder,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] })
      toast.success('Pedido cancelado!', 'O pedido foi cancelado.')
    },
    onError: (error: any) => {
      toast.error('Erro ao cancelar pedido', error.message)
    },
  })
}
Configuração no Layout

src/app/layout.tsx (atualizado)

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { QueryProvider } from '@/providers/query-provider'
import { ToastContainer } from '@/components/atoms/Toast/Toast'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Restaurantix - Dashboard',
  description: 'Sistema de gestão para restaurantes',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pt-BR">
      <body className={inter.className}>
        <QueryProvider>
          <div className="min-h-screen bg-background">
            {children}
          </div>
          <ToastContainer />
        </QueryProvider>
      </body>
    </html>
  )
}

Exemplo de uso em componente

'use client'

import { useState } from 'react'
import { useOrders, useApproveOrder } from '@/hooks/use-orders-queries'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'

export function OrdersList() {
  const [page, setPage] = useState(1)
  const { data, isLoading, error } = useOrders({ page, perPage: 10 })
  const approveOrder = useApproveOrder()

  if (isLoading) {
    return <div>Carregando pedidos...</div>
  }

  if (error) {
    return <div>Erro ao carregar pedidos: {error.message}</div>
  }

  return (
    <div className="space-y-4">
      <h2 className="text-2xl font-bold">Pedidos</h2>
      
      {data?.orders.map((order) => (
        <Card key={order.id}>
          <CardHeader>
            <div className="flex items-center justify-between">
              <CardTitle>Pedido #{order.id.slice(-6)}</CardTitle>
              <Badge variant={order.status === 'pending' ? 'secondary' : 'default'}>
                {order.status}
              </Badge>
            </div>
          </CardHeader>
          <CardContent>
            <div className="flex items-center justify-between">
              <div>
                <p>Cliente: {order.customerName}</p>
                <p>Total: R$ {(order.totalInCents / 100).toFixed(2)}</p>
              </div>
              
              {order.status === 'pending' && (
                <Button
                  onClick={() => approveOrder.mutate(order.id)}
                  disabled={approveOrder.isPending}
                >
                  {approveOrder.isPending ? 'Aprovando...' : 'Aprovar'}
                </Button>
              )}
            </div>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}
🎯 Exercício Prático

Implemente o React Query com API Client:

1. Configurar React Query

Configure o React Query no projeto:

npm install @tanstack/react-query @tanstack/react-query-devtools axios

2. Implementar services

Crie os services para consumir a API:

  • • authService com todas as operações
  • • ordersService com CRUD completo
  • • metricsService para dashboard
  • • Interceptors para auth e errors

3. Criar hooks customizados

Implemente hooks para cada operação:

  • • useOrders com filtros e paginação
  • • useOrderDetails para detalhes
  • • Mutations para aprovar/cancelar pedidos
  • • Cache invalidation automático

💡 Dicas de React Query:

  • • Use queryKey consistentes para cache eficiente
  • • Configure staleTime baseado na frequência de mudança
  • • Implemente optimistic updates para melhor UX
  • • Use React Query DevTools para debug