Quando usar utils e quando usar composables no Nuxt cover image

Quando usar utils e quando usar composables no Nuxt

No Nuxt, tanto utils quanto composables ficam em pastas dedicadas, são autoimportados e parecem igualmente convenientes à primeira vista. Essa conveniência também é onde a confusão começa. Você escreve uma função auxiliar, coloca em utils/, não importa em lugar nenhum e tudo funciona. Uma semana depois precisa de estado reativo ligado à rota e passa a usar composables/. O padrão segue até aparecer um utils/formatCurrency.ts que chama useI18n() em segredo, ou um composables/useSlug.ts que só transforma string em uma linha, sem reatividade.

A diferença não é convenção de pasta. É sobre do que seu código depende e onde ele pode rodar. Utils são funções TypeScript puras. Composables são funções conscientes do Vue que participam de reatividade, ciclo de vida e contexto do Nuxt. Escolher bem mantém o projeto previsível, testável e seguro entre renderização no servidor e no cliente.

O que são utils no Nuxt

Arquivos em utils/ exportam funções independentes. O Nuxt as autoimporta em todo o projeto, então formatDate() de utils/date.ts fica disponível em componentes, rotas de servidor, middleware e plugins sem import explícito.

Como utils não são composables, não devem chamar outros composables. Não devem usar ref(), computed(), watch() nem hooks de ciclo de vida. Também não devem depender de useRoute(), useRuntimeConfig(), useNuxtApp() ou qualquer API que pressuponha um contexto ativo de setup do Vue ou do Nuxt.

Utils são ideais para trabalho determinístico, de entrada para saída:

  • Formatação e parsing: datas, moeda, slugs, telefones
  • Helpers de validação que retornam booleanos ou mensagens de erro
  • Transformações de array e objeto sem efeitos colaterais
  • Tipos TypeScript compartilhados, type guards e algoritmos pequenos
  • Constantes e tabelas de mapeamento usadas no app

Uma util bem escrita é fácil de testar em unitários: você passa argumentos e verifica o retorno. Sem montar componente, sem Nuxt test utils, sem mockar o runtime do Vue.

O que são composables no Nuxt

Composables ficam em composables/ e seguem a convenção use*. São funções pensadas para ser chamadas durante o setup de componentes, inicialização de plugins ou outros contextos em que a Composition API do Vue e o runtime do Nuxt estão disponíveis.

Composables podem:

  • Manter e retornar estado reativo com ref e computed
  • Registrar hooks de ciclo de vida como onMounted e onUnmounted
  • Ler contexto do Nuxt via useRoute, useRouter, useFetch, useState e APIs similares
  • Encapsular comportamento reutilizável que componentes duplicariam
  • Coordenar efeitos colaterais como listeners, timers e subscriptions com cleanup adequado

Pense em composables como lógica de setup reutilizável. Eles respondem perguntas como: como o app busca e cacheia o usuário atual? Como sincronizamos estado de formulário com a URL? Como expor breakpoint de media query como estado reativo?

Um critério prático de decisão

Antes de criar um arquivo novo, faça estas perguntas em ordem.

1. Precisa de reatividade? Se quem consome deve reagir a valores que mudam no tempo, vai para composable. Formatar preço uma vez a partir de uma prop é util. Expor string de busca com debounce que atualiza uma lista é composable.

2. Precisa de contexto do Nuxt ou do Vue? Se sim, é composable. Mesmo um wrapper fino em torno de useFetch pertence a composables/, não a utils/. Código dependente de contexto chamado de dentro de util quebra de forma imprevisível conforme onde e quando a util roda.

3. É uma transformação pura de entradas? Funções puras ficam em utils. Se a mesma lógica poderia viver em um pacote npm sem saber nada de Vue, mantenha-a ali.

4. Vai rodar no servidor fora do setup de componente? Rotas de servidor, handlers Nitro e parte do middleware executam fora do setup do Vue. Utils são seguras nesses lugares. Composables em geral não são, salvo quando você está de propósito em um contexto Nuxt que os suporta.

5. Gerencia efeitos colaterais ou cleanup? Listeners, setInterval, observers e abort controllers precisam de par com ciclo de vida. Isso é território de composable.

Cenários comuns e onde cada um fica

Utils:

  • slugify(title: string): string
  • isValidEmail(value: string): boolean
  • groupBy<T>(items: T[], key: keyof T)
  • formatCurrency(amount: number, locale: string) quando o locale é passado explicitamente

Composables:

  • useAuth() expondo user, login() e logout()
  • usePagination() sincronizando página com query params da rota
  • useDebounceRef() envolvendo um ref com atualização debounced
  • useTheme() lendo preferência e aplicando class em document.documentElement

Repare na divisão: formatar com entradas explícitas é util; formatar lendo o locale ativo de useI18n() é composable, porque depende de contexto de runtime.

Zonas cinzentas e como lidar com elas

Nem todo caso é óbvio. Uma função de fetch que só chama $fetch com URL e opções às vezes pode ficar em util se permanecer livre de contexto. Mas no momento em que você precisa de estado de loading, estado de erro, cache compatível com SSR ou refetch com chave, mova para composable baseado em useFetch ou useAsyncData.

Outra zona cinzenta são regras de negócio compartilhadas. Lógica de domínio que nunca toca Vue deve ficar em utils ou em pasta separada como lib/ ou domain/. A cola da camada de apresentação que combina regras de domínio com estado reativo de UI deve ficar em composables. Manter domínio agnóstico de framework compensa quando você adiciona scripts CLI, jobs no servidor ou testes que não sobem o Nuxt.

Às vezes o time divide a feature de propósito:

  • Util: calculateDiscount(price, rules) — cálculo puro
  • Composable: useCart() — linhas reativas do carrinho, chama a util antes de atualizar totais

Essa separação é saudável. A util permanece portável. O composable cuida de estado e orquestração.

Antipadrões para evitar

  • Utils com cara de composable: arquivo em utils/ que chama useRoute(). Pode funcionar em um componente hoje e falhar em util de servidor amanhã.
  • Composables com cara de util: useFormatSlug(text) que retorna string sem refs nem contexto. Quem consome não ganha nada do padrão composable, só confusão.
  • Estado global escondido em utils: variáveis mutáveis em nível de módulo dentro de utils criam efeitos fantasmas e quebram premissas de SSR.
  • Composables deus: um useApp() que busca usuário, carrinho, notificações e tema. Prefira composables focados que se combinam.

Implicações para SSR e testes

O Nuxt renderiza no servidor primeiro. Utils puras se comportam igual em servidor e cliente. Composables que tocam APIs só do browser devem proteger com import.meta.client ou rodar dentro de onMounted. Colocar essa proteção no composable é correto. Espalhar checagens de DOM em utils aleatórias é sinal de que o código está na camada errada.

Testes seguem a mesma fronteira. Utils ganham unitários rápidos. Composables ganham testes com @vue/test-utils ou helpers do Nuxt quando coordenam ciclo de vida e contexto. Se você não consegue testar uma util sem montar Vue, provavelmente ela é composable.

Nomenclatura e descoberta

Siga convenções para o time adivinhar onde o arquivo está:

  • Utils: verbos ou frases nominais — parseQuery, clamp, buildBreadcrumb
  • Composables: sempre prefixo useuseSearch, useClipboard, useConfirmDialog

O auto-import faz a localização importar. Um esquema de nomes consistente transforma a escolha de pasta em documentação.

Regra prática

Use util quando tiver função pura que transforma dados e pode rodar em qualquer parte da stack. Use composable quando estiver empacotando estado reativo, contexto do Nuxt, ciclo de vida ou efeitos colaterais para reutilizar entre componentes.

Na dúvida, comece mais restrito: escreva a menor util pura possível. Promova a composable só quando reatividade ou contexto exigirem. Esse caminho mantém o projeto Nuxt fácil de entender conforme cresce e evita debugar erros de contexto de setup em lugares que deveriam ter permanecido funções simples.