Voltar ao blog
nuxtvuearquiteturalayersmultitenanttypescriptsaas

Refatorei a arquitetura do meu SaaS com Nuxt Layers antes de precisar

15 de junho de 2026 às 00:17 - Por Larissa Santos

Refatorei a arquitetura do meu SaaS com Nuxt Layers antes de precisar

Recentemente me deparei com um artigo técnico sobre Nuxt Layers, uma forma de trabalhar com arquitetura modular no Nuxt. Eu ainda não havia implementado esse formato em nenhum projeto, mas durante a leitura percebi que ele fazia bastante sentido para um SaaS multitenant em que estou trabalhando.

Se você ainda não conhece o projeto, recomendo ler primeiro o artigo anterior, onde explico a arquitetura inicial dele. Este texto é uma continuação dessa evolução, com foco na decisão de reorganizar o projeto usando Layers.


A estrutura que eu tinha

A organização inicial seguia a estrutura clássica de um projeto Nuxt, separando os arquivos por tipo:

txt
app/
├── components/
│   ├── admin/
│   ├── catalog/
│   ├── checkout/
│   ├── landingpage/
│   ├── pedido/
│   └── shared/
├── composables/
│   ├── admin/
│   ├── catalog/
│   ├── checkout/
│   └── pedido/
├── layouts/
│   ├── admin.vue
│   ├── auth.vue
│   └── catalog.vue
├── stores/
│   ├── useCarrinhoStore.ts
│   ├── useEmpresaStore.ts
│   └── usePedidosStore.ts
├── pages/
│   ├── index.vue
│   ├── 404.vue
│   ├── [slug]/
│   │   ├── index.vue
│   │   ├── checkout.vue
│   │   └── acompanhar-pedido.vue
│   └── admin/
│       ├── index.vue
│       ├── acervo.vue
│       ├── categorias.vue
│       ├── login.vue
│       └── pedidos.vue
└── middleware/
    ├── auth.ts
    └── tenant.global.ts

Ela estava organizada e funcionando. O ponto é que, conforme o projeto crescia, comecei a perceber que algumas funcionalidades estavam espalhadas por várias partes do app/.

Por exemplo, para mexer no checkout, eu precisava abrir arquivos em components/checkout/, composables/checkout/, stores/useCarrinhoStore.ts e pages/[slug]/checkout.vue. Tudo fazia parte da mesma responsabilidade, mas estava distribuído em pastas diferentes.

Isso ainda não era um problema real. A aplicação funcionava bem. A decisão de mudar foi mais preventiva: reorganizar a arquitetura antes que o projeto crescesse o suficiente para tornar essa separação mais difícil.


Por que Layers fizeram sentido para esse projeto

A ideia que mais fez sentido para mim no conceito de Nuxt Layers foi organizar o projeto por domínio de negócio, e não apenas por tipo de arquivo.

No meu caso, o projeto tinha três contextos bem definidos:

  • Cliente final, com vitrine pública, checkout e acompanhamento de pedido
  • Admin da empresa, com painel de gestão do acervo e dos pedidos
  • Super Admin, planejado para uma fase futura, voltado à gestão dos tenants

Cada contexto tem seus próprios componentes, suas próprias regras, seus próprios layouts e suas próprias páginas. Eles fazem parte da mesma aplicação, mas representam áreas diferentes do produto.

Foi isso que fez a arquitetura com Layers encaixar. A intenção não era aplicar uma estrutura nova apenas porque parecia interessante, mas porque ela refletia melhor a forma como o projeto estava dividido na prática.


Como desenhei a nova estrutura

A partir disso, separei os principais domínios em layers independentes:

txt
layers/
├── vitrine/
│   ├── components/
│   │   ├── catalog/
│   │   └── checkout/
│   ├── composables/
│   │   ├── catalog/
│   │   └── checkout/
│   ├── layouts/
│   │   └── catalog.vue
│   ├── stores/
│   │   ├── useCarrinhoStore.ts
│   │   └── usePedidosStore.ts
│   ├── pages/
│   │   └── [slug]/
│   │       ├── index.vue
│   │       ├── checkout.vue
│   │       └── acompanhar-pedido.vue
│   └── nuxt.config.ts
├── admin/
│   ├── components/
│   ├── composables/
│   ├── layouts/
│   │   ├── admin.vue
│   │   └── auth.vue
│   ├── middleware/
│   │   └── auth.ts
│   ├── pages/
│   │   ├── dashboard.vue
│   │   ├── acervo.vue
│   │   ├── categorias.vue
│   │   ├── login.vue
│   │   └── pedidos.vue
│   └── nuxt.config.ts
└── landingpage/
    ├── components/
    ├── pages/
    │   └── index.vue
    └── nuxt.config.ts

app/
├── components/shared/
├── composables/
├── stores/
├── middleware/
├── pages/
│   └── 404.vue
├── error.vue
└── app.vue

Mantive no app/ aquilo que é realmente global da aplicação, como arquivos base, middleware global, stores compartilhadas e componentes comuns. As layers ficaram responsáveis pelos domínios principais do produto, ou seja, partes que têm fluxo, responsabilidade e contexto próprios.

Uma das decisões mais importantes foi manter catalog e checkout na mesma layer, chamada vitrine. Tecnicamente, eu poderia separar o checkout em uma layer própria, mas no projeto isso não faria tanto sentido.

As páginas do checkout vivem dentro de [slug], o mesmo contexto da vitrine pública. Além disso, o carrinho depende diretamente dos dados do catálogo. Se eu separasse em duas layers, provavelmente criaria uma fronteira que eu precisaria atravessar o tempo todo.

Por isso, vitrine ficou como a jornada completa do cliente final: navegar pelo acervo, adicionar itens ao carrinho, finalizar o pedido e acompanhar o status.

Também movi os layouts para dentro das layers correspondentes:

  • admin.vue e auth.vue ficaram em layers/admin/layouts/
  • catalog.vue ficou em layers/vitrine/layouts/

Essa escolha deixou cada domínio mais autocontido. Quando abro a layer admin, encontro páginas, layouts, componentes e lógica do painel no mesmo lugar.


Configurando as Layers no Nuxt

No nuxt.config.ts principal, registrei as layers usando extends:

ts
export default defineNuxtConfig({
  extends: [
    './layers/admin',
    './layers/landingpage',
    './layers/vitrine',
  ]
})

A ordem aqui importa. Como a layer vitrine possui rotas dinâmicas com [slug], ela precisa ficar por último. Caso contrário, o Nuxt pode interpretar /admin como slug = 'admin' antes de encontrar a rota estática do painel.

Cada layer também recebeu seu próprio nuxt.config.ts. Em alguns casos, ele ficou praticamente vazio. Em outros, como no admin, precisei configurar auto-imports, prefixo de componentes e ajuste das rotas.

Na layer admin, a configuração ficou assim:

ts
// layers/admin/nuxt.config.ts
export default defineNuxtConfig({
  imports: {
    dirs: ['composables/**', 'stores'],
  },
  components: [
    { path: './components', prefix: 'Adm' }
  ],
  hooks: {
    'pages:extend'(pages) {
      pages.forEach((page) => {
        if (page.file?.includes('/layers/admin/pages/') && page.path !== '/admin') {
          page.path = '/admin' + page.path
        }
      })
    }
  }
})

O imports.dirs com composables/** permite manter composables organizados em subpastas, como composables/acervo/, composables/pedidos/ ou composables/categorias/, sem perder o auto-import.

O components com prefixo Adm evita colisão de nomes entre layers, pois o Nuxt faz o auto-import dos componentes com base no nome dos arquivos e pastas. Como diferentes layers podem ter componentes com nomes iguais, como Header.vue, Footer.vue ou Button.vue, deixar tudo com nomes genéricos poderia causar conflitos ou dificultar entender qual componente está sendo usado em cada contexto.

O hook pages:extend resolveu outro ponto de organização. Sem ele, para ter rotas como /admin/login, eu precisaria criar uma estrutura como layers/admin/pages/admin/login.vue. Funcionaria, mas ficaria repetitivo. Com o hook, as páginas continuam na raiz de pages/ da layer admin, e o Nuxt prefixa as rotas com /admin.

Na layer landingpage, usei a mesma ideia de prefixo para evitar nomes genéricos:

ts
// layers/landingpage/nuxt.config.ts
export default defineNuxtConfig({
  components: [
    { path: './components', prefix: 'Lp' }
  ]
})

Dessa forma, um componente como layers/landingpage/components/Hero.vue passa a ser usado como <LpHero />.


Como fiz a migração

Fiz a migração uma layer por vez. A ideia foi reduzir risco e validar cada parte antes de seguir para a próxima.

Comecei pela landingpage, porque era a parte mais isolada. Ela não dependia diretamente de stores complexas, checkout, carrinho ou middleware específico. Isso tornou a primeira migração mais simples e ajudou a entender como o Nuxt resolveria os componentes dentro da layer.

Depois fui para o admin, porque ele tinha um domínio claro e bem separado da vitrine pública. Nesse ponto apareceram decisões mais interessantes, principalmente nas rotas.

As páginas do admin estavam originalmente em app/pages/admin/. Ao mover para layers/admin/pages/, se eu deixasse os arquivos soltos, eles seriam registrados como /login, /pedidos, /acervo e assim por diante. Eu queria manter as URLs com /admin, mas sem criar layers/admin/pages/admin/.

Foi aí que usei o hook pages:extend. Ele permitiu manter a organização interna da layer sem repetir o nome da pasta na estrutura.

Outra decisão importante foi trocar o antigo index.vue do admin por dashboard.vue. Como a landingpage já possuía um index.vue, os dois arquivos inicialmente competiam pelo mesmo path / durante a geração das rotas.

Ao renomear a página do admin e declarar path: '/admin' com definePageMeta, deixei a rota explícita e removi essa ambiguidade da resolução.

vue
<script setup lang="ts">
definePageMeta({
  layout: 'admin',
  middleware: 'auth',
  path: '/admin',
})
</script>

Para as demais páginas, como login.vue, pedidos.vue e acervo.vue, deixei o hook cuidar do prefixo /admin.

Além de resolver o conflito, o nome dashboard.vue ficou mais semântico do que index.vue.

Por fim, migrei a vitrine, que era a parte mais sensível por envolver [slug], catálogo, carrinho, checkout e pedidos. Deixei essa etapa por último justamente porque ela concentra a jornada principal do cliente final.

Os commits também acompanharam essa separação por domínio:

bash
git commit -m "chore: migra landingpage para layer dedicada"
git commit -m "chore: migra painel admin para layer dedicada"
git commit -m "chore: migra vitrine (catalog + checkout + pedido) para layer dedicada"

O resultado

A principal diferença agora é que cada domínio tem um local próprio.

Quando preciso mexer no checkout, abro layers/vitrine/ e encontro os componentes, composables, stores, páginas e layout relacionados à jornada do cliente. Quando preciso mexer no painel, vou para layers/admin/. Quando preciso alterar a página institucional, vou para layers/landingpage/.

Isso não torna o projeto mais simples, ainda existe complexidade. Mas agora ela está melhor distribuída.

A estrutura também fica mais preparada para o roadmap. Quando a fase de relatórios e contratos PDF chegar, posso avaliar se faz sentido criar uma nova layer. Quando o Super Admin sair do planejamento e virar código, ele também pode nascer como um domínio próprio.


Quando Layers fazem sentido e quando são overkill

Depois de aplicar no projeto, minha percepção é que Layers são úteis quando existe uma divisão real de domínio. Não é uma estrutura a ser usada em qualquer projeto Nuxt.

Fazem sentido quando

  • Você tem contextos de usuário distintos que raramente se cruzam
  • O projeto é multitenant e possui domínios bem definidos
  • O roadmap aponta para crescimento real, novas features e novos contextos
  • Você quer que uma pessoa nova consiga entender um domínio sem precisar conhecer o projeto inteiro
  • Componentes, páginas, stores e composables de uma mesma funcionalidade estão ficando espalhados demais

Podem ser overkill quando

  • O projeto é um MVP ou protótipo e velocidade importa mais que estrutura
  • Você tem poucos componentes e tudo ainda cabe facilmente na estrutura padrão
  • É um site institucional ou landing page sem domínios de negócio reais
  • O time é pequeno, o prazo é curto e não existe previsão de crescimento
  • A separação criaria mais configuração do que benefício real

Se o projeto for um CRUD simples com uma área administrativa básica, a estrutura clássica do Nuxt resolve muito bem. Layers não precisam entrar em tudo. Arquitetura também é saber quando não adicionar complexidade.


A pergunta que guiou a decisão

A mesma pergunta do artigo anterior continua válida aqui:

Se eu precisar mudar isso daqui a seis meses, qual é o menor número de arquivos que vou precisar tocar?

Quando a estrutura está bem definida, a resposta tende a ser menor. Quando começa a ser vários arquivos espalhados em pastas diferentes, talvez alguma responsabilidade esteja no lugar errado.

No meu caso, Nuxt Layers ajudou a organizar melhor essas responsabilidades antes que a estrutura inicial trouxesse uma limitação.


Leituras relacionadas

LarissaSantos

Desenvolvedora Frontend apaixonada por criar experiências digitais incríveis

Navegação

2026 © Larissa Santos Feito com Vue.js