Skip to main content
Este guia assume que você leu o Setup do Frontend e já tem o projeto rodando localmente.

Visão geral do sistema

O OmniDom é uma plataforma de gestão para sellers de marketplace. É composta de dois repositórios independentes:
RepositórioTecnologiaPorta padrãoResponsabilidade
hub-marketplaceNext.js 163001Interface do usuário (SPA)
Backend (NestJS)NestJS + PostgreSQL3000API REST + regras de negócio
O frontend nunca implementa regras de negócio. Todas as validações de domínio (ex: produto do tipo KIT precisa de componentes) são responsabilidade do backend e comunicadas via respostas da API.

Como é uma feature completa

Toda funcionalidade no frontend segue este fluxo:
1. Usuário interage com um componente
2. Componente chama um hook (ex: useCreateProduct)
3. Hook chama useMutation do TanStack Query
4. useMutation chama o service (ex: productService.create)
5. Service faz a chamada Axios
6. Axios interceptor injeta token + tenant automaticamente
7. Resposta retorna ao componente via hook

Criando uma nova feature

1. Criar a estrutura de pastas

src/features/<nome-da-feature>/
├── components/
├── hooks/
├── services/
├── types/
└── schemas.ts      (se tiver formulários)

2. Definir os tipos

// src/features/suppliers/types/index.ts
export interface Supplier {
  id: string
  name: string
  document: string
  createdAt: string
}

export interface CreateSupplierDTO {
  name: string
  document: string
}

3. Criar o service

// src/features/suppliers/services/supplier.service.ts
import api from '@/shared/lib/axios'
import { Supplier, CreateSupplierDTO } from '../types'

export const supplierService = {
  list: () =>
    api.get<Supplier[]>('/suppliers').then(r => r.data),

  create: (data: CreateSupplierDTO) =>
    api.post<Supplier>('/suppliers', data).then(r => r.data),

  delete: (id: string) =>
    api.delete(`/suppliers/${id}`).then(r => r.data),
}

4. Criar os hooks

// src/features/suppliers/hooks/useSuppliers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { supplierService } from '../services/supplier.service'

export function useSuppliers() {
  return useQuery({
    queryKey: ['suppliers'],
    queryFn: supplierService.list,
  })
}

export function useCreateSupplier() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: supplierService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['suppliers'] })
    },
  })
}

5. Criar os componentes

// src/features/suppliers/components/SupplierList.tsx
'use client'

import { useSuppliers } from '../hooks/useSuppliers'

export function SupplierList() {
  const { data, isLoading } = useSuppliers()

  if (isLoading) return <p>Carregando...</p>

  return (
    <ul>
      {data?.map(s => <li key={s.id}>{s.name}</li>)}
    </ul>
  )
}

6. Criar a página no App Router

// src/app/(protected)/suppliers/page.tsx
import { SupplierList } from '@/features/suppliers/components/SupplierList'

export default function SuppliersPage() {
  return (
    <div>
      <h1>Fornecedores</h1>
      <SupplierList />
    </div>
  )
}
Note que a página (page.tsx) é um Server Component por padrão. O 'use client' fica apenas nos componentes que têm interatividade. Neste exemplo, o SupplierList usa hooks, por isso é client.

Pontos críticos para entender

Autenticação (leia com atenção)

O token de acesso fica em memória (Zustand). Ao recarregar a página (F5), o Zustand é zerado. O ProtectedLayout detecta isso e faz um silent refresh automático usando o cookie HttpOnly. Isso significa:
  • Nunca salve o token em localStorage — quebrará a segurança.
  • Não tente ler o token diretamente — use useAuthStore().accessToken se precisar.
  • O interceptor Axios cuida de tudo automaticamente.

Multi-tenancy

Cada usuário pertence a um tenant. O tenantId é resolvido no login e armazenado no useAuthStore (via user.tenantId) e também no useTenantStore. O interceptor Axios injeta o header x-tenant-id em todas as requisições automaticamente. Você não precisa fazer nada para isso funcionar nas suas chamadas de API.

Paginação

A API usa cursor-based pagination para todas as listagens. Nunca implemente paginação por offset (page 1, página 2). Use o cursor retornado pelo backend para carregar mais itens.

Padrões a seguir

SituaçãoFazerNão fazer
Chamada HTTPCriar service em services/Usar Axios diretamente no componente
Estado do servidorTanStack Query (useQuery/useMutation)useState + useEffect + fetch
Estado de UI localuseStateZustand com estado efêmero
Estado global persistenteZustandContext API para estado frequente
Validação de formulárioZod schemaValidação manual com if
Regra de negócioConfiar na resposta da APIReimplementar lógica do backend
EstilizaçãoTailwind + cn() utilityInline styles, CSS modules

Boas práticas de PR

  1. Um PR por feature ou fix — PRs grandes são difíceis de revisar.
  2. Teste o fluxo completo antes de abrir o PR — login, autenticação e a funcionalidade nova.
  3. Tipos explícitos — sem any não justificado.
  4. Nomeação descritivauseCreateProduct, não useForm.
  5. Sem console.log em código de produção.

Perguntas frequentes

No backend. O frontend envia os dados e o backend retorna erro se a regra for violada. O frontend apenas exibe o erro ao usuário.
import { useTenantStore } from '@/features/tenant/store/tenant-store'
const tenant = useTenantStore(s => s.tenant)
O tenantId também está disponível em useAuthStore().user?.tenantId.
O projeto já tem swr como dependência, mas o padrão estabelecido é TanStack Query. Use TanStack Query para novas features para manter consistência.
Crie a pasta e o page.tsx dentro de src/app/(protected)/. A proteção é automática pelo ProtectedLayout.
TanStack Query expõe isError e error no hook. Use sonner para notificações toast:
const { mutate, isError } = useCreateProduct()
// No onError do useMutation:
onError: (err) => toast.error(err.response?.data?.message ?? 'Erro ao criar produto')