O que melhores-práticas-para-storefront faz?
Sempre use esta habilidade ao trabalhar em vitrines de e-commerce, lojas online, sites de compras. Use para QUALQUER componente de vitrine, incluindo páginas de checkout, carrinho, fluxos de pagamento, páginas de produto, listagens de produtos, navegação, página inicial ou QUALQUER página/componente em uma vitrine. CRÍTICO para adicionar checkout, implementar carrinho, integrar backend Medusa ou construir qualquer funcionalidade de e-commerce. Agnóstico de framework (Next.js, SvelteKit, TanStack Start, React, Vue). Fornece padrões, frameworks de decisão e orientação de integração com backend.
Visão Geral
Este guia consolida as melhores práticas para construir vitrines de e-commerce headless com Medusa. Ele complementa os guias oficiais do Medusa e oferece padrões prontos para produção, frameworks de decisão para lidar com trade-offs comuns e orientação de integração de backend. O objetivo é ajudar equipes de engenharia a criar experiências de compras de alta qualidade, com bom desempenho e fáceis de manter.
Público-Alvo
Desenvolvedores de frontend e full-stack que:
- Estão construindo uma nova vitrine Medusa do zero
- Estão migrando uma loja existente para Medusa
- Precisam de orientação sobre padrões de implementação, desempenho ou acessibilidade
- Querem entender como estruturar componentes de e-commerce complexos (carrinho, checkout, páginas de produto)
Como Usar Este Guia
Cada seção descreve uma área-chave de funcionalidade de e-commerce. Para sua tarefa atual, faça o seguinte:
- Navegue até a seção relevante
- Revise os padrões recomendados e frameworks de decisão
- Aplique os padrões ao seu código, adaptando conforme necessário para seu caso de uso específico
Índice
- Integração com Backend
- Estrutura de Loja
- Componentes de Vitrine
- Carrinho e Checkout
- Componentes de Produto
- Tratamento de Erros e Estados de Carregamento
- Desempenho e Cache
- Preços e Moeda
- Integrações de Pagamento
- SEO e Metadados
- Conclusão
1. Integração com Backend
1.1. Conectando ao Backend Medusa
Use o SDK oficial @medusajs/medusa-js ou o pacote React @medusajs/medusa-react para conectar sua vitrine ao backend Medusa.
Para projetos React/Next.js:
import { MedusaProvider } from 'medusa-react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { CartProvider, SessionProvider } from 'medusa-react'
const queryClient = new QueryClient()
const backUrl = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL || 'http://localhost:9000'
function MyApp({ Component, pageProps }) {
return (
<MedusaProvider
baseUrl={backUrl}
queryClientProviderProps={{
client: queryClient,
}}
>
<SessionProvider>
<CartProvider>
<Component {...pageProps} />
</CartProvider>
</SessionProvider>
</MedusaProvider>
)
}
Para projetos não-React (Vanilla JS, Vue, Svelte, etc.):
import Medusa from '@medusajs/medusa-js'
const medusa = new Medusa({ baseUrl: 'http://localhost:9000', maxRetries: 3 })
// Exemplo: buscar produtos
const { products, count, limit, offset } = await medusa.products.list()
1.2. Gerenciamento de Estado do Cliente
Medusa usa sessões anônimas para rastrear o carrinho antes do login e carrinhos autenticados após o login.
Padrão: Sempre inicialize uma sessão de cliente anônima assim que o usuário carregar a página.
// React com medusa-react
import { useSession } from 'medusa-react'
function StorefrontApp() {
const { customer, isLoading } = useSession()
// customer é null para usuários anônimos
// A sessão é gerenciada automaticamente pelo CartProvider
}
Práticas Recomendadas:
- Não force login para navegar ou adicionar ao carrinho. Permita compras como convidado.
- Armazene o ID de carrinho anônimo em cookie ou localStorage para que persista entre sessões.
- Ao fazer login, mescle o carrinho anônimo com a conta do cliente (Medusa faz isso automaticamente via
medusa.auth.createSessioncom callback).
1.3. Estratégias de Autenticação
Fluxo de Login Padrão:
- Usuário insere e-mail e senha
- Chamar
medusa.auth.createSessionpara autenticar - A sessão é armazenada automaticamente em cookie pelo backend
- O
SessionProviderem medusa-react detecta o cookie e atualiza o estado do cliente
Registro de Novo Cliente:
const { customer } = await medusa.customers.create({
email: '[email protected]',
first_name: 'John',
last_name: 'Doe',
password: 'securepassword'
})
// Isso automaticamente cria e autentica uma sessão
1.4. Tratamento de Erros de API
Use um interceptador global ou wrapper para tratar erros de API de forma consistente:
async function fetchWithErrorHandling(url, options) {
try {
const response = await fetch(url, options)
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || 'Erro desconhecido')
}
return await response.json()
} catch (error) {
console.error('Erro na requisição:', error)
// Notificar o usuário ou exibir estado de erro
throw error
}
}
2. Estrutura de Loja
2.1. Arquitetura de Páginas Recomendada
Organize sua vitrine com uma estrutura de páginas clara. Exemplo para Next.js:
/pages
_app.js - Provedores globais (MedusaProvider, CartProvider, SessionProvider)
index.js - Página inicial (produtos em destaque, hero, etc.)
/products
index.js - Listagem de produtos (grade, filtros, paginação)
[handle].js - Página de detalhes do produto
/cart
index.js - Página do carrinho (itens, quantidades, subtotal, cupom)
/checkout
index.js - Fluxo de checkout (endereço, envio, pagamento, confirmação)
/account
index.js - Painel da conta do cliente (pedidos, detalhes)
2.2. Contextos e Provedores
Gerencie estado global com provedores React (ou equivalentes em outros frameworks):
// Em _app.js
function MyApp({ Component, pageProps }) {
return (
<MedusaProvider baseUrl={MEDUSA_BACKEND_URL}>
<SessionProvider>
<CartProvider>
<Component {...pageProps} />
</CartProvider>
</SessionProvider>
</MedusaProvider>
)
}
Funções dos Provedores:
MedusaProvider: Fornece o cliente Medusa e integra React Query para cacheSessionProvider: Gerencia estado de sessão anônima vs autenticadaCartProvider: Gerencia estado do carrinho (conteúdo, quantidades, etc.)
3. Componentes de Vitrine
3.1. Componentes Reutilizáveis vs Específicos
Componentes Reutilizáveis:
- Botões (add-to-cart, checkout, etc.)
- Campos de formulário (quantidade, variante)
- Cards de produto (imagem, título, preço, ação)
- Componentes de navegação (menu, breadcrumbs)
Componentes Específicos de Página:
- Página de listagem de produtos (busca, filtros, grade)
- Página de detalhes do produto (galeria, informações, seleção de variante)
- Fluxo de checkout (multi-etapas, validação)
3.2. Padrão para Componentes de Produto
Cada componente de produto deve ser o mais granular possível para reutilização:
// ProductCard.jsx
import { useCart } from 'medusa-react'
export default function ProductCard({ product }) {
const { addItem } = useCart()
const handleAddToCart = () => {
// Usar a primeira variante disponível ou a selecionada
const variant = product.variants[0]
addItem({
variant_id: variant.id,
quantity: 1
})
}
return (
<div className="product-card">
<Image src={product.thumbnail} alt={product.title} />
<h3>{product.title}</h3>
<p>{product.description}</p>
<button onClick={handleAddToCart}>Adicionar ao Carrinho</button>
</div>
)
}
4. Carrinho e Checkout
4.1. Gerenciamento de Carrinho
Use useCart() hook do medusa-react para gerenciar estado do carrinho:
function CartPage() {
const { cart, addItem, updateItem, removeItem, setCart } = useCart()
// cart.items contém todos os itens do carrinho
// cart.total é o total calculado (inclui impostos, envio)
const handleUpdateQuantity = (itemId, quantity) => {
updateItem({ line_id: itemId, quantity })
}
const handleRemoveItem = (itemId) => {
removeItem({ line_id: itemId })
}
if (!cart) return <p>Carregando carrinho...</p>
if (cart.items.length === 0) return <p>Seu carrinho está vazio.</p>
return (
<div>
{cart.items.map(item => (
<CartItem
key={item.id}
item={item}
onUpdateQuantity={handleUpdateQuantity}
onRemove={handleRemoveItem}
/>
))}
<p>Subtotal: {cart.subtotal}</p>
<p>Total: {cart.total}</p>
<Link href="/checkout">Ir para Checkout</Link>
</div>
)
}
4.2. Fluxo de Checkout
Etapas Recomendadas:
- Endereço de Envio - Coletar e validar endereço
- Método de Envio - Selecionar opção de envio disponível
- Pagamento - Selecionar método de pagamento e inserir detalhes
- Revisão - Revisar pedido antes de finalizar
- Confirmação - Confirmar pedido e exibir ID do pedido
Exemplo de Criação de Pedido (última etapa do checkout):
import { useCart, useMeCustomer } from 'medusa-react'
import { useRouter } from 'next/router'
function CheckoutConfirmation() {
const { cart, completeCart } = useCart()
const router = useRouter()
const handlePlaceOrder = async () => {
try {
const { order, type } = await completeCart()
if (type === 'order') {
// Pedido criado com sucesso
router.push(`/order/confirmed/${order.id}`)
} else if (type === 'cart') {
// Cart ainda não está pronto para checkout (exige mais informações)
console.log('Carrinho incompleto:', cart)
}
} catch (error) {
console.error('Falha ao finalizar pedido:', error)
// Exibir mensagem de erro ao usuário
}
}
return (
<div>
<h2>Revisar Pedido</h2>
<p>Endereço de Envio: {cart.shipping_address?.address_1}</p>
<p>Método de Envio: {cart.shipping_methods?.[0]?.name}</p>
<p>Total: {cart.total}</p>
<button onClick={handlePlaceOrder}>Finalizar Pedido</button>
</div>
)
}
4.3. Cupons de Desconto
Permita que os usuários apliquem cupons no carrinho:
function CouponField() {
const { cart, updateCart } = useCart()
const [couponCode, setCouponCode] = useState('')
const handleApplyCoupon = () => {
updateCart({
discounts: [{ code: couponCode }]
})
setCouponCode('')
}
return (
<div>
<input
type="text"
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
placeholder="Digite seu cupom"
/>
<button onClick={handleApplyCoupon}>Aplicar</button>
</div>
)
}
5. Componentes de Produto
5.1. Listagem de Produtos
Paginação e Filtros:
import { useProducts } from 'medusa-react'
import { useState } from 'react'
function ProductListing({ collectionHandle }) {
const [page, setPage] = useState(0)
const PAGE_SIZE = 12
const { products, count, isLoading } = useProducts({
limit: PAGE_SIZE,
offset: page * PAGE_SIZE,
collection_id: [collectionHandle] // ou use collection_id
})
const totalPages = Math.ceil(count / PAGE_SIZE)
if (isLoading) return <p>Carregando produtos...</p>
return (
<div>
<div className="product-grid">
{products?.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
<div className="pagination">
{Array.from({ length: totalPages }, (_, i) => (
<button key={i} onClick={() => setPage(i)} disabled={i === page}>
{i + 1}
</button>
))}
</div>
</div>
)
}
5.2. Página de Detalhes do Produto
Seleção de Variante e Galeria:
import { useProduct } from 'medusa-react'
import { useState } from 'react'
function ProductDetails({ handle }) {
const { product, isLoading } = useProduct(handle)
const [selectedVariant, setSelectedVariant] = useState(null)
if (isLoading) return <p>Carregando produto...</p>
if (!product) return <p>Produto não encontrado.</p>
// Usar a primeira variante se nenhuma estiver selecionada
const variant = selectedVariant || product.variants[0]
return (
<div>
<div className="product-gallery">
<img src={variant.thumbnail || product.thumbnail} alt={product.title} />
{/* Miniaturas de imagens */}
</div>
<div className="product-info">
<h1>{product.title}</h1>
<p>{product.description}</p>
<p className="price">{variant.prices[0].amount} {variant.prices[0].currency_code}</p>
<div className="variant-selector">
{product.variants.map(v => (
<button
key={v.id}
onClick={() => setSelectedVariant(v)}
className={v.id === variant.id ? 'selected' : ''}
>
{v.title}
</button>
))}
</div>
<AddToCartButton variant={variant} />
</div>
</div>
)
}
6. Tratamento de Erros e Estados de Carregamento
6.1. Padrão de Carregamento
Sempre forneça um feedback visual durante carregamentos de dados:
function ProductPage() {
const { product, isLoading, isError } = useProduct(handle)
if (isLoading) return <Skeleton />
if (isError) return <ErrorState message="Falha ao carregar produto" />
if (!product) return <NotFound />
return <ProductDetails product={product} />
}
6.2. Tratamento de Erros Específicos
Erros de Estoque:
function AddToCartButton({ variant }) {
const { cart, addItem } = useCart()
const [error, setError] = useState(null)
const handleAddToCart = async () => {
try {
if (variant.inventory_quantity <= 0) {
throw new Error('Produto fora de estoque')
}
await addItem({
variant_id: variant.id,
quantity: 1
})
} catch (err) {
setError(err.message)
}
}
return (
<div>
<button onClick={handleAddToCart}>
{variant.inventory_quantity > 0 ? 'Adicionar ao Carrinho' : 'Fora de Estoque'}
</button>
{error && <p className="error">{error}</p>}
</div>
)
}
7. Desempenho e Cache
7.1. Estratégia de Cache com Medusa React
Medusa React usa React Query (TanStack Query) que gerencia cache automático:
// Configurações de cache
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutos antes de revalidar
cacheTime: 1000 * 60 * 30, // Armazenar em cache por 30 minutos
refetchOnWindowFocus: false, // Evitar refetch em foco de janela
},
},
})
7.2. Otimização de Imagens
- Use Image component com lazy loading
- Sirva imagens via CDN (Medusa aceita URLs externas)
- Defina tamanhos adequados no thumbnail e imagens
import Image from 'next/image'
function ProductImage({ src, alt, width, height }) {
return (
<Image
src={src || '/placeholder.png'}
alt={alt}
width={width || 400}
height={height || 400}
lazy="loading"
priority={false}
/>
)
}
7.3. Prefetching
Para navegação instantânea, prefetch dados de páginas que o usuário provavelmente visitará:
import { useQueryClient } from '@tanstack/react-query'
function ProductCard({ product }) {
const queryClient = useQueryClient()
const prefetchProduct = () => {
queryClient.prefetchQuery({
queryKey: ['product', product.handle],
queryFn: () => medusa.products.retrieve(product.handle)
})
}
return (
<Link
href={`/products/${product.handle}`}
onMouseEnter={prefetchProduct}
>
<h3>{product.title}</h3>
</Link>
)
}
8. Preços e Moeda
8.1. Exibição de Preço
Medusa suporta múltiplas moedas e regiões. Sempre exiba o preço no formato apropriado:
function PriceDisplay({ amount, currencyCode }) {
const formattedPrice = new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: currencyCode || 'BRL'
}).format(amount / 100) // Medusa armazena preços em centavos
return <span>{formattedPrice}</span>
}
8.2. Preços com Desconto
Se houver um preço original e um preço promocional, exiba ambos:
function PriceWithDiscount({ price, originalPrice, currencyCode }) {
const discountPercentage = Math.round((1 - price / originalPrice) * 100)
return (
<div>
<span className="original-price" style={{ textDecoration: 'line-through' }}>
<PriceDisplay amount={originalPrice} currencyCode={currencyCode} />
</span>
<span className="sale-price">
<PriceDisplay amount={price} currencyCode={currencyCode} />
</span>
{discountPercentage > 0 && <span className="discount-badge">-{discountPercentage}%</span>}
</div>
)
}
9. Integrações de Pagamento
9.1. Configuração de Métodos de Pagamento
Medusa suporta vários provedores de pagamento (Stripe, PayPal, etc.). Configure no backend:
// No arquivo medusa-config.js
const plugins = [
{
resolve: `medusa-payment-stripe`,
options: {
api_key: process.env.STRIPE_API_KEY,
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
},
},
]
9.2. Coleta de Pagamento no Frontend
Para Stripe, use o Stripe Elements ou Stripe Checkout:
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'
function PaymentForm() {
const stripe = useStripe()
const elements = useElements()
const handleSubmit = async (event) => {
event.preventDefault()
if (!stripe || !elements) return
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardElement),
})
if (error) {
console.error('Erro de pagamento:', error)
} else {
// Enviar paymentMethod.id para o backend Medusa
await medusa.carts.setPaymentSession(cart.id, {
provider_id: 'stripe',
data: { payment_method_id: paymentMethod.id }
})
}
}
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit" disabled={!stripe}>
Pagar
</button>
</form>
)
}
10. SEO e Metadados
10.1. Metadados por Página
Use tags <head> apropriadas para SEO, Open Graph e Twitter Cards:
import Head from 'next/head'
function ProductPage({ product }) {
return (
<>
<Head>
<title>{product.title} | Minha Loja</title>
<meta name="description" content={product.description} />
<meta property="og:title" content={product.title} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.thumbnail} />
<meta name="twitter:card" content="summary_large_image" />
</Head>
{/* Conteúdo da página */}
</>
)
}
10.2. URLs Amigáveis
Use handle (slug) do produto para URLs limpas e amigáveis:
// Em Next.js, use [handle].js
// URL: /products/camiseta-basica
11. Conclusão
Este guia fornece as bases para construir uma vitrine Medusa performática e fácil de manter. Lembre-se:
- Consulte sempre as referências oficiais do Medusa para detalhes atualizados
- Adapte os padrões ao seu framework e caso de uso
- Teste fluxos de checkout e pagamento completamente
- Monitore desempenho e otimize conforme necessário
Para mais informações, visite docs.medusajs.com.