[{"data":1,"prerenderedAt":1693},["ShallowReactive",2],{"blog-sass-nuxt-layers":3},{"id":4,"title":5,"author":6,"body":7,"date":1677,"description":1678,"extension":1679,"image":1680,"meta":1681,"navigation":564,"path":1682,"seo":1683,"stem":1684,"tags":1685,"__hash__":1692},"blog\u002Fblog\u002Fsass-nuxt-layers.md","Refatorei a arquitetura do meu SaaS com Nuxt Layers antes de precisar","Larissa Santos",{"type":8,"value":9,"toc":1663},"minimark",[10,27,30,33,38,41,272,279,297,300,302,306,313,316,338,341,344,346,350,353,616,622,636,643,649,652,675,682,684,688,699,789,806,812,818,1107,1128,1137,1157,1163,1232,1242,1244,1248,1251,1257,1263,1290,1296,1311,1321,1426,1441,1449,1458,1461,1520,1522,1526,1529,1543,1546,1549,1551,1555,1558,1563,1580,1584,1601,1604,1606,1610,1613,1618,1621,1624,1626,1630,1659],[11,12,13,14,21,22,26],"p",{},"Recentemente me deparei com um ",[15,16,20],"a",{"href":17,"rel":18},"https:\u002F\u002Fwww.gabrielcaiana.com\u002Fpt\u002Fblog\u002Farquitetura-modular-frontend-nuxt-layers-revoluciona-organizacao-projetos-vue\u002F",[19],"nofollow","artigo técnico"," sobre ",[23,24,25],"strong",{},"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.",[11,28,29],{},"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.",[31,32],"hr",{},[34,35,37],"h2",{"id":36},"a-estrutura-que-eu-tinha","A estrutura que eu tinha",[11,39,40],{},"A organização inicial seguia a estrutura clássica de um projeto Nuxt, separando os arquivos por tipo:",[42,43,48],"pre",{"className":44,"code":45,"language":46,"meta":47,"style":47},"language-txt shiki shiki-themes material-theme-lighter github-dark github-dark","app\u002F\n├── components\u002F\n│   ├── admin\u002F\n│   ├── catalog\u002F\n│   ├── checkout\u002F\n│   ├── landingpage\u002F\n│   ├── pedido\u002F\n│   └── shared\u002F\n├── composables\u002F\n│   ├── admin\u002F\n│   ├── catalog\u002F\n│   ├── checkout\u002F\n│   └── pedido\u002F\n├── layouts\u002F\n│   ├── admin.vue\n│   ├── auth.vue\n│   └── catalog.vue\n├── stores\u002F\n│   ├── useCarrinhoStore.ts\n│   ├── useEmpresaStore.ts\n│   └── usePedidosStore.ts\n├── pages\u002F\n│   ├── index.vue\n│   ├── 404.vue\n│   ├── [slug]\u002F\n│   │   ├── index.vue\n│   │   ├── checkout.vue\n│   │   └── acompanhar-pedido.vue\n│   └── admin\u002F\n│       ├── index.vue\n│       ├── acervo.vue\n│       ├── categorias.vue\n│       ├── login.vue\n│       └── pedidos.vue\n└── middleware\u002F\n    ├── auth.ts\n    └── tenant.global.ts\n","txt","",[49,50,51,59,65,71,77,83,89,95,101,107,112,117,122,128,134,140,146,152,158,164,170,176,182,188,194,200,206,212,218,224,230,236,242,248,254,260,266],"code",{"__ignoreMap":47},[52,53,56],"span",{"class":54,"line":55},"line",1,[52,57,58],{},"app\u002F\n",[52,60,62],{"class":54,"line":61},2,[52,63,64],{},"├── components\u002F\n",[52,66,68],{"class":54,"line":67},3,[52,69,70],{},"│   ├── admin\u002F\n",[52,72,74],{"class":54,"line":73},4,[52,75,76],{},"│   ├── catalog\u002F\n",[52,78,80],{"class":54,"line":79},5,[52,81,82],{},"│   ├── checkout\u002F\n",[52,84,86],{"class":54,"line":85},6,[52,87,88],{},"│   ├── landingpage\u002F\n",[52,90,92],{"class":54,"line":91},7,[52,93,94],{},"│   ├── pedido\u002F\n",[52,96,98],{"class":54,"line":97},8,[52,99,100],{},"│   └── shared\u002F\n",[52,102,104],{"class":54,"line":103},9,[52,105,106],{},"├── composables\u002F\n",[52,108,110],{"class":54,"line":109},10,[52,111,70],{},[52,113,115],{"class":54,"line":114},11,[52,116,76],{},[52,118,120],{"class":54,"line":119},12,[52,121,82],{},[52,123,125],{"class":54,"line":124},13,[52,126,127],{},"│   └── pedido\u002F\n",[52,129,131],{"class":54,"line":130},14,[52,132,133],{},"├── layouts\u002F\n",[52,135,137],{"class":54,"line":136},15,[52,138,139],{},"│   ├── admin.vue\n",[52,141,143],{"class":54,"line":142},16,[52,144,145],{},"│   ├── auth.vue\n",[52,147,149],{"class":54,"line":148},17,[52,150,151],{},"│   └── catalog.vue\n",[52,153,155],{"class":54,"line":154},18,[52,156,157],{},"├── stores\u002F\n",[52,159,161],{"class":54,"line":160},19,[52,162,163],{},"│   ├── useCarrinhoStore.ts\n",[52,165,167],{"class":54,"line":166},20,[52,168,169],{},"│   ├── useEmpresaStore.ts\n",[52,171,173],{"class":54,"line":172},21,[52,174,175],{},"│   └── usePedidosStore.ts\n",[52,177,179],{"class":54,"line":178},22,[52,180,181],{},"├── pages\u002F\n",[52,183,185],{"class":54,"line":184},23,[52,186,187],{},"│   ├── index.vue\n",[52,189,191],{"class":54,"line":190},24,[52,192,193],{},"│   ├── 404.vue\n",[52,195,197],{"class":54,"line":196},25,[52,198,199],{},"│   ├── [slug]\u002F\n",[52,201,203],{"class":54,"line":202},26,[52,204,205],{},"│   │   ├── index.vue\n",[52,207,209],{"class":54,"line":208},27,[52,210,211],{},"│   │   ├── checkout.vue\n",[52,213,215],{"class":54,"line":214},28,[52,216,217],{},"│   │   └── acompanhar-pedido.vue\n",[52,219,221],{"class":54,"line":220},29,[52,222,223],{},"│   └── admin\u002F\n",[52,225,227],{"class":54,"line":226},30,[52,228,229],{},"│       ├── index.vue\n",[52,231,233],{"class":54,"line":232},31,[52,234,235],{},"│       ├── acervo.vue\n",[52,237,239],{"class":54,"line":238},32,[52,240,241],{},"│       ├── categorias.vue\n",[52,243,245],{"class":54,"line":244},33,[52,246,247],{},"│       ├── login.vue\n",[52,249,251],{"class":54,"line":250},34,[52,252,253],{},"│       └── pedidos.vue\n",[52,255,257],{"class":54,"line":256},35,[52,258,259],{},"└── middleware\u002F\n",[52,261,263],{"class":54,"line":262},36,[52,264,265],{},"    ├── auth.ts\n",[52,267,269],{"class":54,"line":268},37,[52,270,271],{},"    └── tenant.global.ts\n",[11,273,274,275,278],{},"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 ",[49,276,277],{},"app\u002F",".",[11,280,281,282,285,286,285,289,292,293,296],{},"Por exemplo, para mexer no checkout, eu precisava abrir arquivos em ",[49,283,284],{},"components\u002Fcheckout\u002F",", ",[49,287,288],{},"composables\u002Fcheckout\u002F",[49,290,291],{},"stores\u002FuseCarrinhoStore.ts"," e ",[49,294,295],{},"pages\u002F[slug]\u002Fcheckout.vue",". Tudo fazia parte da mesma responsabilidade, mas estava distribuído em pastas diferentes.",[11,298,299],{},"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.",[31,301],{},[34,303,305],{"id":304},"por-que-layers-fizeram-sentido-para-esse-projeto","Por que Layers fizeram sentido para esse projeto",[11,307,308,309,312],{},"A ideia que mais fez sentido para mim no conceito de Nuxt Layers foi organizar o projeto por ",[23,310,311],{},"domínio de negócio",", e não apenas por tipo de arquivo.",[11,314,315],{},"No meu caso, o projeto tinha três contextos bem definidos:",[317,318,319,326,332],"ul",{},[320,321,322,325],"li",{},[23,323,324],{},"Cliente final",", com vitrine pública, checkout e acompanhamento de pedido",[320,327,328,331],{},[23,329,330],{},"Admin da empresa",", com painel de gestão do acervo e dos pedidos",[320,333,334,337],{},[23,335,336],{},"Super Admin",", planejado para uma fase futura, voltado à gestão dos tenants",[11,339,340],{},"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.",[11,342,343],{},"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.",[31,345],{},[34,347,349],{"id":348},"como-desenhei-a-nova-estrutura","Como desenhei a nova estrutura",[11,351,352],{},"A partir disso, separei os principais domínios em layers independentes:",[42,354,356],{"className":44,"code":355,"language":46,"meta":47,"style":47},"layers\u002F\n├── vitrine\u002F\n│   ├── components\u002F\n│   │   ├── catalog\u002F\n│   │   └── checkout\u002F\n│   ├── composables\u002F\n│   │   ├── catalog\u002F\n│   │   └── checkout\u002F\n│   ├── layouts\u002F\n│   │   └── catalog.vue\n│   ├── stores\u002F\n│   │   ├── useCarrinhoStore.ts\n│   │   └── usePedidosStore.ts\n│   ├── pages\u002F\n│   │   └── [slug]\u002F\n│   │       ├── index.vue\n│   │       ├── checkout.vue\n│   │       └── acompanhar-pedido.vue\n│   └── nuxt.config.ts\n│\n├── admin\u002F\n│   ├── components\u002F\n│   ├── composables\u002F\n│   ├── layouts\u002F\n│   │   ├── admin.vue\n│   │   └── auth.vue\n│   ├── middleware\u002F\n│   │   └── auth.ts\n│   ├── pages\u002F\n│   │   ├── dashboard.vue\n│   │   ├── acervo.vue\n│   │   ├── categorias.vue\n│   │   ├── login.vue\n│   │   └── pedidos.vue\n│   └── nuxt.config.ts\n│\n└── landingpage\u002F\n    ├── components\u002F\n    ├── pages\u002F\n    │   └── index.vue\n    └── nuxt.config.ts\n\napp\u002F\n├── components\u002Fshared\u002F\n├── composables\u002F\n├── stores\u002F\n├── middleware\u002F\n├── pages\u002F\n│   └── 404.vue\n├── error.vue\n└── app.vue\n",[49,357,358,363,368,373,378,383,388,392,396,401,406,411,416,421,426,431,436,441,446,451,456,461,465,469,473,478,483,488,493,497,502,507,512,517,522,526,530,535,541,547,553,559,566,571,577,582,587,593,598,604,610],{"__ignoreMap":47},[52,359,360],{"class":54,"line":55},[52,361,362],{},"layers\u002F\n",[52,364,365],{"class":54,"line":61},[52,366,367],{},"├── vitrine\u002F\n",[52,369,370],{"class":54,"line":67},[52,371,372],{},"│   ├── components\u002F\n",[52,374,375],{"class":54,"line":73},[52,376,377],{},"│   │   ├── catalog\u002F\n",[52,379,380],{"class":54,"line":79},[52,381,382],{},"│   │   └── checkout\u002F\n",[52,384,385],{"class":54,"line":85},[52,386,387],{},"│   ├── composables\u002F\n",[52,389,390],{"class":54,"line":91},[52,391,377],{},[52,393,394],{"class":54,"line":97},[52,395,382],{},[52,397,398],{"class":54,"line":103},[52,399,400],{},"│   ├── layouts\u002F\n",[52,402,403],{"class":54,"line":109},[52,404,405],{},"│   │   └── catalog.vue\n",[52,407,408],{"class":54,"line":114},[52,409,410],{},"│   ├── stores\u002F\n",[52,412,413],{"class":54,"line":119},[52,414,415],{},"│   │   ├── useCarrinhoStore.ts\n",[52,417,418],{"class":54,"line":124},[52,419,420],{},"│   │   └── usePedidosStore.ts\n",[52,422,423],{"class":54,"line":130},[52,424,425],{},"│   ├── pages\u002F\n",[52,427,428],{"class":54,"line":136},[52,429,430],{},"│   │   └── [slug]\u002F\n",[52,432,433],{"class":54,"line":142},[52,434,435],{},"│   │       ├── index.vue\n",[52,437,438],{"class":54,"line":148},[52,439,440],{},"│   │       ├── checkout.vue\n",[52,442,443],{"class":54,"line":154},[52,444,445],{},"│   │       └── acompanhar-pedido.vue\n",[52,447,448],{"class":54,"line":160},[52,449,450],{},"│   └── nuxt.config.ts\n",[52,452,453],{"class":54,"line":166},[52,454,455],{},"│\n",[52,457,458],{"class":54,"line":172},[52,459,460],{},"├── admin\u002F\n",[52,462,463],{"class":54,"line":178},[52,464,372],{},[52,466,467],{"class":54,"line":184},[52,468,387],{},[52,470,471],{"class":54,"line":190},[52,472,400],{},[52,474,475],{"class":54,"line":196},[52,476,477],{},"│   │   ├── admin.vue\n",[52,479,480],{"class":54,"line":202},[52,481,482],{},"│   │   └── auth.vue\n",[52,484,485],{"class":54,"line":208},[52,486,487],{},"│   ├── middleware\u002F\n",[52,489,490],{"class":54,"line":214},[52,491,492],{},"│   │   └── auth.ts\n",[52,494,495],{"class":54,"line":220},[52,496,425],{},[52,498,499],{"class":54,"line":226},[52,500,501],{},"│   │   ├── dashboard.vue\n",[52,503,504],{"class":54,"line":232},[52,505,506],{},"│   │   ├── acervo.vue\n",[52,508,509],{"class":54,"line":238},[52,510,511],{},"│   │   ├── categorias.vue\n",[52,513,514],{"class":54,"line":244},[52,515,516],{},"│   │   ├── login.vue\n",[52,518,519],{"class":54,"line":250},[52,520,521],{},"│   │   └── pedidos.vue\n",[52,523,524],{"class":54,"line":256},[52,525,450],{},[52,527,528],{"class":54,"line":262},[52,529,455],{},[52,531,532],{"class":54,"line":268},[52,533,534],{},"└── landingpage\u002F\n",[52,536,538],{"class":54,"line":537},38,[52,539,540],{},"    ├── components\u002F\n",[52,542,544],{"class":54,"line":543},39,[52,545,546],{},"    ├── pages\u002F\n",[52,548,550],{"class":54,"line":549},40,[52,551,552],{},"    │   └── index.vue\n",[52,554,556],{"class":54,"line":555},41,[52,557,558],{},"    └── nuxt.config.ts\n",[52,560,562],{"class":54,"line":561},42,[52,563,565],{"emptyLinePlaceholder":564},true,"\n",[52,567,569],{"class":54,"line":568},43,[52,570,58],{},[52,572,574],{"class":54,"line":573},44,[52,575,576],{},"├── components\u002Fshared\u002F\n",[52,578,580],{"class":54,"line":579},45,[52,581,106],{},[52,583,585],{"class":54,"line":584},46,[52,586,157],{},[52,588,590],{"class":54,"line":589},47,[52,591,592],{},"├── middleware\u002F\n",[52,594,596],{"class":54,"line":595},48,[52,597,181],{},[52,599,601],{"class":54,"line":600},49,[52,602,603],{},"│   └── 404.vue\n",[52,605,607],{"class":54,"line":606},50,[52,608,609],{},"├── error.vue\n",[52,611,613],{"class":54,"line":612},51,[52,614,615],{},"└── app.vue\n",[11,617,618,619,621],{},"Mantive no ",[49,620,277],{}," 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.",[11,623,624,625,292,628,631,632,635],{},"Uma das decisões mais importantes foi manter ",[49,626,627],{},"catalog",[49,629,630],{},"checkout"," na mesma layer, chamada ",[49,633,634],{},"vitrine",". Tecnicamente, eu poderia separar o checkout em uma layer própria, mas no projeto isso não faria tanto sentido.",[11,637,638,639,642],{},"As páginas do checkout vivem dentro de ",[49,640,641],{},"[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.",[11,644,645,646,648],{},"Por isso, ",[49,647,634],{}," ficou como a jornada completa do cliente final: navegar pelo acervo, adicionar itens ao carrinho, finalizar o pedido e acompanhar o status.",[11,650,651],{},"Também movi os layouts para dentro das layers correspondentes:",[317,653,654,666],{},[320,655,656,292,659,662,663],{},[49,657,658],{},"admin.vue",[49,660,661],{},"auth.vue"," ficaram em ",[49,664,665],{},"layers\u002Fadmin\u002Flayouts\u002F",[320,667,668,671,672],{},[49,669,670],{},"catalog.vue"," ficou em ",[49,673,674],{},"layers\u002Fvitrine\u002Flayouts\u002F",[11,676,677,678,681],{},"Essa escolha deixou cada domínio mais autocontido. Quando abro a layer ",[49,679,680],{},"admin",", encontro páginas, layouts, componentes e lógica do painel no mesmo lugar.",[31,683],{},[34,685,687],{"id":686},"configurando-as-layers-no-nuxt","Configurando as Layers no Nuxt",[11,689,690,691,694,695,698],{},"No ",[49,692,693],{},"nuxt.config.ts"," principal, registrei as layers usando ",[49,696,697],{},"extends",":",[42,700,704],{"className":701,"code":702,"language":703,"meta":47,"style":47},"language-ts shiki shiki-themes material-theme-lighter github-dark github-dark","export default defineNuxtConfig({\n  extends: [\n    '.\u002Flayers\u002Fadmin',\n    '.\u002Flayers\u002Flandingpage',\n    '.\u002Flayers\u002Fvitrine',\n  ]\n})\n","ts",[49,705,706,727,738,754,765,776,781],{"__ignoreMap":47},[52,707,708,712,715,719,723],{"class":54,"line":55},[52,709,711],{"class":710},"s3Er8","export",[52,713,714],{"class":710}," default",[52,716,718],{"class":717},"sK_r7"," defineNuxtConfig",[52,720,722],{"class":721},"sMo7A","(",[52,724,726],{"class":725},"sG-J9","{\n",[52,728,729,733,735],{"class":54,"line":61},[52,730,732],{"class":731},"sdv8B","  extends",[52,734,698],{"class":725},[52,736,737],{"class":721}," [\n",[52,739,740,744,748,751],{"class":54,"line":67},[52,741,743],{"class":742},"sF_wb","    '",[52,745,747],{"class":746},"s0vBq",".\u002Flayers\u002Fadmin",[52,749,750],{"class":742},"'",[52,752,753],{"class":725},",\n",[52,755,756,758,761,763],{"class":54,"line":73},[52,757,743],{"class":742},[52,759,760],{"class":746},".\u002Flayers\u002Flandingpage",[52,762,750],{"class":742},[52,764,753],{"class":725},[52,766,767,769,772,774],{"class":54,"line":79},[52,768,743],{"class":742},[52,770,771],{"class":746},".\u002Flayers\u002Fvitrine",[52,773,750],{"class":742},[52,775,753],{"class":725},[52,777,778],{"class":54,"line":85},[52,779,780],{"class":721},"  ]\n",[52,782,783,786],{"class":54,"line":91},[52,784,785],{"class":725},"}",[52,787,788],{"class":721},")\n",[11,790,791,792,794,795,797,798,801,802,805],{},"A ordem aqui importa. Como a layer ",[49,793,634],{}," possui rotas dinâmicas com ",[49,796,641],{},", ela precisa ficar por último. Caso contrário, o Nuxt pode interpretar ",[49,799,800],{},"\u002Fadmin"," como ",[49,803,804],{},"slug = 'admin'"," antes de encontrar a rota estática do painel.",[11,807,808,809,811],{},"Cada layer também recebeu seu próprio ",[49,810,693],{},". Em alguns casos, ele ficou praticamente vazio. Em outros, como no admin, precisei configurar auto-imports, prefixo de componentes e ajuste das rotas.",[11,813,814,815,817],{},"Na layer ",[49,816,680],{},", a configuração ficou assim:",[42,819,821],{"className":701,"code":820,"language":703,"meta":47,"style":47},"\u002F\u002F layers\u002Fadmin\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n  imports: {\n    dirs: ['composables\u002F**', 'stores'],\n  },\n  components: [\n    { path: '.\u002Fcomponents', prefix: 'Adm' }\n  ],\n  hooks: {\n    'pages:extend'(pages) {\n      pages.forEach((page) => {\n        if (page.file?.includes('\u002Flayers\u002Fadmin\u002Fpages\u002F') && page.path !== '\u002Fadmin') {\n          page.path = '\u002Fadmin' + page.path\n        }\n      })\n    }\n  }\n})\n",[49,822,823,829,841,851,884,889,898,932,939,948,968,993,1051,1079,1084,1091,1096,1101],{"__ignoreMap":47},[52,824,825],{"class":54,"line":55},[52,826,828],{"class":827},"sutJx","\u002F\u002F layers\u002Fadmin\u002Fnuxt.config.ts\n",[52,830,831,833,835,837,839],{"class":54,"line":61},[52,832,711],{"class":710},[52,834,714],{"class":710},[52,836,718],{"class":717},[52,838,722],{"class":721},[52,840,726],{"class":725},[52,842,843,846,848],{"class":54,"line":67},[52,844,845],{"class":731},"  imports",[52,847,698],{"class":725},[52,849,850],{"class":725}," {\n",[52,852,853,856,858,861,863,866,868,871,874,877,879,882],{"class":54,"line":73},[52,854,855],{"class":731},"    dirs",[52,857,698],{"class":725},[52,859,860],{"class":721}," [",[52,862,750],{"class":742},[52,864,865],{"class":746},"composables\u002F**",[52,867,750],{"class":742},[52,869,870],{"class":725},",",[52,872,873],{"class":742}," '",[52,875,876],{"class":746},"stores",[52,878,750],{"class":742},[52,880,881],{"class":721},"]",[52,883,753],{"class":725},[52,885,886],{"class":54,"line":79},[52,887,888],{"class":725},"  },\n",[52,890,891,894,896],{"class":54,"line":85},[52,892,893],{"class":731},"  components",[52,895,698],{"class":725},[52,897,737],{"class":721},[52,899,900,903,906,908,910,913,915,917,920,922,924,927,929],{"class":54,"line":91},[52,901,902],{"class":725},"    {",[52,904,905],{"class":731}," path",[52,907,698],{"class":725},[52,909,873],{"class":742},[52,911,912],{"class":746},".\u002Fcomponents",[52,914,750],{"class":742},[52,916,870],{"class":725},[52,918,919],{"class":731}," prefix",[52,921,698],{"class":725},[52,923,873],{"class":742},[52,925,926],{"class":746},"Adm",[52,928,750],{"class":742},[52,930,931],{"class":725}," }\n",[52,933,934,937],{"class":54,"line":97},[52,935,936],{"class":721},"  ]",[52,938,753],{"class":725},[52,940,941,944,946],{"class":54,"line":103},[52,942,943],{"class":731},"  hooks",[52,945,698],{"class":725},[52,947,850],{"class":725},[52,949,950,952,955,957,959,963,966],{"class":54,"line":109},[52,951,743],{"class":742},[52,953,954],{"class":746},"pages:extend",[52,956,750],{"class":742},[52,958,722],{"class":725},[52,960,962],{"class":961},"sk1zL","pages",[52,964,965],{"class":725},")",[52,967,850],{"class":725},[52,969,970,973,975,978,980,982,985,987,991],{"class":54,"line":114},[52,971,972],{"class":721},"      pages",[52,974,278],{"class":725},[52,976,977],{"class":717},"forEach",[52,979,722],{"class":731},[52,981,722],{"class":725},[52,983,984],{"class":961},"page",[52,986,965],{"class":725},[52,988,990],{"class":989},"sFsEu"," =>",[52,992,850],{"class":725},[52,994,995,998,1001,1003,1005,1008,1011,1014,1016,1018,1021,1023,1026,1030,1033,1035,1038,1041,1043,1045,1047,1049],{"class":54,"line":119},[52,996,997],{"class":710},"        if",[52,999,1000],{"class":731}," (",[52,1002,984],{"class":721},[52,1004,278],{"class":725},[52,1006,1007],{"class":721},"file",[52,1009,1010],{"class":725},"?.",[52,1012,1013],{"class":717},"includes",[52,1015,722],{"class":731},[52,1017,750],{"class":742},[52,1019,1020],{"class":746},"\u002Flayers\u002Fadmin\u002Fpages\u002F",[52,1022,750],{"class":742},[52,1024,1025],{"class":731},") ",[52,1027,1029],{"class":1028},"sFfmW","&&",[52,1031,1032],{"class":721}," page",[52,1034,278],{"class":725},[52,1036,1037],{"class":721},"path",[52,1039,1040],{"class":1028}," !==",[52,1042,873],{"class":742},[52,1044,800],{"class":746},[52,1046,750],{"class":742},[52,1048,1025],{"class":731},[52,1050,726],{"class":725},[52,1052,1053,1056,1058,1060,1063,1065,1067,1069,1072,1074,1076],{"class":54,"line":124},[52,1054,1055],{"class":721},"          page",[52,1057,278],{"class":725},[52,1059,1037],{"class":721},[52,1061,1062],{"class":1028}," =",[52,1064,873],{"class":742},[52,1066,800],{"class":746},[52,1068,750],{"class":742},[52,1070,1071],{"class":1028}," +",[52,1073,1032],{"class":721},[52,1075,278],{"class":725},[52,1077,1078],{"class":721},"path\n",[52,1080,1081],{"class":54,"line":130},[52,1082,1083],{"class":725},"        }\n",[52,1085,1086,1089],{"class":54,"line":136},[52,1087,1088],{"class":725},"      }",[52,1090,788],{"class":731},[52,1092,1093],{"class":54,"line":142},[52,1094,1095],{"class":725},"    }\n",[52,1097,1098],{"class":54,"line":148},[52,1099,1100],{"class":725},"  }\n",[52,1102,1103,1105],{"class":54,"line":154},[52,1104,785],{"class":725},[52,1106,788],{"class":721},[11,1108,1109,1110,1113,1114,1116,1117,285,1120,1123,1124,1127],{},"O ",[49,1111,1112],{},"imports.dirs"," com ",[49,1115,865],{}," permite manter composables organizados em subpastas, como ",[49,1118,1119],{},"composables\u002Facervo\u002F",[49,1121,1122],{},"composables\u002Fpedidos\u002F"," ou ",[49,1125,1126],{},"composables\u002Fcategorias\u002F",", sem perder o auto-import.",[11,1129,1109,1130,1133,1134,1136],{},[49,1131,1132],{},"components"," com prefixo ",[49,1135,926],{}," 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.",[11,1138,1139,1140,1142,1143,1146,1147,1150,1151,1154,1155,278],{},"O hook ",[49,1141,954],{}," resolveu outro ponto de organização. Sem ele, para ter rotas como ",[49,1144,1145],{},"\u002Fadmin\u002Flogin",", eu precisaria criar uma estrutura como ",[49,1148,1149],{},"layers\u002Fadmin\u002Fpages\u002Fadmin\u002Flogin.vue",". Funcionaria, mas ficaria repetitivo. Com o hook, as páginas continuam na raiz de ",[49,1152,1153],{},"pages\u002F"," da layer admin, e o Nuxt prefixa as rotas com ",[49,1156,800],{},[11,1158,814,1159,1162],{},[49,1160,1161],{},"landingpage",", usei a mesma ideia de prefixo para evitar nomes genéricos:",[42,1164,1166],{"className":701,"code":1165,"language":703,"meta":47,"style":47},"\u002F\u002F layers\u002Flandingpage\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n  components: [\n    { path: '.\u002Fcomponents', prefix: 'Lp' }\n  ]\n})\n",[49,1167,1168,1173,1185,1193,1222,1226],{"__ignoreMap":47},[52,1169,1170],{"class":54,"line":55},[52,1171,1172],{"class":827},"\u002F\u002F layers\u002Flandingpage\u002Fnuxt.config.ts\n",[52,1174,1175,1177,1179,1181,1183],{"class":54,"line":61},[52,1176,711],{"class":710},[52,1178,714],{"class":710},[52,1180,718],{"class":717},[52,1182,722],{"class":721},[52,1184,726],{"class":725},[52,1186,1187,1189,1191],{"class":54,"line":67},[52,1188,893],{"class":731},[52,1190,698],{"class":725},[52,1192,737],{"class":721},[52,1194,1195,1197,1199,1201,1203,1205,1207,1209,1211,1213,1215,1218,1220],{"class":54,"line":73},[52,1196,902],{"class":725},[52,1198,905],{"class":731},[52,1200,698],{"class":725},[52,1202,873],{"class":742},[52,1204,912],{"class":746},[52,1206,750],{"class":742},[52,1208,870],{"class":725},[52,1210,919],{"class":731},[52,1212,698],{"class":725},[52,1214,873],{"class":742},[52,1216,1217],{"class":746},"Lp",[52,1219,750],{"class":742},[52,1221,931],{"class":725},[52,1223,1224],{"class":54,"line":79},[52,1225,780],{"class":721},[52,1227,1228,1230],{"class":54,"line":85},[52,1229,785],{"class":725},[52,1231,788],{"class":721},[11,1233,1234,1235,1238,1239,278],{},"Dessa forma, um componente como ",[49,1236,1237],{},"layers\u002Flandingpage\u002Fcomponents\u002FHero.vue"," passa a ser usado como ",[49,1240,1241],{},"\u003CLpHero \u002F>",[31,1243],{},[34,1245,1247],{"id":1246},"como-fiz-a-migração","Como fiz a migração",[11,1249,1250],{},"Fiz a migração uma layer por vez. A ideia foi reduzir risco e validar cada parte antes de seguir para a próxima.",[11,1252,1253,1254,1256],{},"Comecei pela ",[49,1255,1161],{},", 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.",[11,1258,1259,1260,1262],{},"Depois fui para o ",[49,1261,680],{},", porque ele tinha um domínio claro e bem separado da vitrine pública. Nesse ponto apareceram decisões mais interessantes, principalmente nas rotas.",[11,1264,1265,1266,1269,1270,1273,1274,285,1277,285,1280,1283,1284,1286,1287,278],{},"As páginas do admin estavam originalmente em ",[49,1267,1268],{},"app\u002Fpages\u002Fadmin\u002F",". Ao mover para ",[49,1271,1272],{},"layers\u002Fadmin\u002Fpages\u002F",", se eu deixasse os arquivos soltos, eles seriam registrados como ",[49,1275,1276],{},"\u002Flogin",[49,1278,1279],{},"\u002Fpedidos",[49,1281,1282],{},"\u002Facervo"," e assim por diante. Eu queria manter as URLs com ",[49,1285,800],{},", mas sem criar ",[49,1288,1289],{},"layers\u002Fadmin\u002Fpages\u002Fadmin\u002F",[11,1291,1292,1293,1295],{},"Foi aí que usei o hook ",[49,1294,954],{},". Ele permitiu manter a organização interna da layer sem repetir o nome da pasta na estrutura.",[11,1297,1298,1299,1302,1303,1306,1307,1310],{},"Outra decisão importante foi trocar o antigo ",[49,1300,1301],{},"index.vue"," do admin por ",[49,1304,1305],{},"dashboard.vue",". Como a landingpage já possuía um index.vue, os dois arquivos inicialmente competiam pelo mesmo path ",[49,1308,1309],{},"\u002F"," durante a geração das rotas.",[11,1312,1313,1314,1113,1317,1320],{},"Ao renomear a página do admin e declarar ",[49,1315,1316],{},"path: '\u002Fadmin'",[49,1318,1319],{},"definePageMeta",", deixei a rota explícita e removi essa ambiguidade da resolução.",[42,1322,1326],{"className":1323,"code":1324,"language":1325,"meta":47,"style":47},"language-vue shiki shiki-themes material-theme-lighter github-dark github-dark","\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n  layout: 'admin',\n  middleware: 'auth',\n  path: '\u002Fadmin',\n})\n\u003C\u002Fscript>\n","vue",[49,1327,1328,1357,1365,1380,1396,1411,1417],{"__ignoreMap":47},[52,1329,1330,1333,1337,1341,1344,1347,1350,1352,1354],{"class":54,"line":55},[52,1331,1332],{"class":725},"\u003C",[52,1334,1336],{"class":1335},"sqIbZ","script",[52,1338,1340],{"class":1339},"s7047"," setup",[52,1342,1343],{"class":1339}," lang",[52,1345,1346],{"class":725},"=",[52,1348,1349],{"class":742},"\"",[52,1351,703],{"class":746},[52,1353,1349],{"class":742},[52,1355,1356],{"class":725},">\n",[52,1358,1359,1361,1363],{"class":54,"line":61},[52,1360,1319],{"class":717},[52,1362,722],{"class":721},[52,1364,726],{"class":725},[52,1366,1367,1370,1372,1374,1376,1378],{"class":54,"line":67},[52,1368,1369],{"class":731},"  layout",[52,1371,698],{"class":725},[52,1373,873],{"class":742},[52,1375,680],{"class":746},[52,1377,750],{"class":742},[52,1379,753],{"class":725},[52,1381,1382,1385,1387,1389,1392,1394],{"class":54,"line":73},[52,1383,1384],{"class":731},"  middleware",[52,1386,698],{"class":725},[52,1388,873],{"class":742},[52,1390,1391],{"class":746},"auth",[52,1393,750],{"class":742},[52,1395,753],{"class":725},[52,1397,1398,1401,1403,1405,1407,1409],{"class":54,"line":79},[52,1399,1400],{"class":731},"  path",[52,1402,698],{"class":725},[52,1404,873],{"class":742},[52,1406,800],{"class":746},[52,1408,750],{"class":742},[52,1410,753],{"class":725},[52,1412,1413,1415],{"class":54,"line":85},[52,1414,785],{"class":725},[52,1416,788],{"class":721},[52,1418,1419,1422,1424],{"class":54,"line":91},[52,1420,1421],{"class":725},"\u003C\u002F",[52,1423,1336],{"class":1335},[52,1425,1356],{"class":725},[11,1427,1428,1429,285,1432,292,1435,1438,1439,278],{},"Para as demais páginas, como ",[49,1430,1431],{},"login.vue",[49,1433,1434],{},"pedidos.vue",[49,1436,1437],{},"acervo.vue,"," deixei o hook cuidar do prefixo ",[49,1440,800],{},[11,1442,1443,1444,1446,1447,278],{},"Além de resolver o conflito, o nome ",[49,1445,1305],{}," ficou mais semântico do que ",[49,1448,1301],{},[11,1450,1451,1452,1454,1455,1457],{},"Por fim, migrei a ",[49,1453,634],{},", que era a parte mais sensível por envolver ",[49,1456,641],{},", catálogo, carrinho, checkout e pedidos. Deixei essa etapa por último justamente porque ela concentra a jornada principal do cliente final.",[11,1459,1460],{},"Os commits também acompanharam essa separação por domínio:",[42,1462,1466],{"className":1463,"code":1464,"language":1465,"meta":47,"style":47},"language-bash shiki shiki-themes material-theme-lighter github-dark github-dark","git commit -m \"chore: migra landingpage para layer dedicada\"\ngit commit -m \"chore: migra painel admin para layer dedicada\"\ngit commit -m \"chore: migra vitrine (catalog + checkout + pedido) para layer dedicada\"\n","bash",[49,1467,1468,1490,1505],{"__ignoreMap":47},[52,1469,1470,1474,1477,1481,1484,1487],{"class":54,"line":55},[52,1471,1473],{"class":1472},"soiBB","git",[52,1475,1476],{"class":746}," commit",[52,1478,1480],{"class":1479},"sSJ72"," -m",[52,1482,1483],{"class":742}," \"",[52,1485,1486],{"class":746},"chore: migra landingpage para layer dedicada",[52,1488,1489],{"class":742},"\"\n",[52,1491,1492,1494,1496,1498,1500,1503],{"class":54,"line":61},[52,1493,1473],{"class":1472},[52,1495,1476],{"class":746},[52,1497,1480],{"class":1479},[52,1499,1483],{"class":742},[52,1501,1502],{"class":746},"chore: migra painel admin para layer dedicada",[52,1504,1489],{"class":742},[52,1506,1507,1509,1511,1513,1515,1518],{"class":54,"line":67},[52,1508,1473],{"class":1472},[52,1510,1476],{"class":746},[52,1512,1480],{"class":1479},[52,1514,1483],{"class":742},[52,1516,1517],{"class":746},"chore: migra vitrine (catalog + checkout + pedido) para layer dedicada",[52,1519,1489],{"class":742},[31,1521],{},[34,1523,1525],{"id":1524},"o-resultado","O resultado",[11,1527,1528],{},"A principal diferença agora é que cada domínio tem um local próprio.",[11,1530,1531,1532,1535,1536,1539,1540,278],{},"Quando preciso mexer no checkout, abro ",[49,1533,1534],{},"layers\u002Fvitrine\u002F"," e encontro os componentes, composables, stores, páginas e layout relacionados à jornada do cliente. Quando preciso mexer no painel, vou para ",[49,1537,1538],{},"layers\u002Fadmin\u002F",". Quando preciso alterar a página institucional, vou para ",[49,1541,1542],{},"layers\u002Flandingpage\u002F",[11,1544,1545],{},"Isso não torna o projeto mais simples, ainda existe complexidade. Mas agora ela está melhor distribuída.",[11,1547,1548],{},"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.",[31,1550],{},[34,1552,1554],{"id":1553},"quando-layers-fazem-sentido-e-quando-são-overkill","Quando Layers fazem sentido e quando são overkill",[11,1556,1557],{},"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.",[1559,1560,1562],"h3",{"id":1561},"fazem-sentido-quando","Fazem sentido quando",[317,1564,1565,1568,1571,1574,1577],{},[320,1566,1567],{},"Você tem contextos de usuário distintos que raramente se cruzam",[320,1569,1570],{},"O projeto é multitenant e possui domínios bem definidos",[320,1572,1573],{},"O roadmap aponta para crescimento real, novas features e novos contextos",[320,1575,1576],{},"Você quer que uma pessoa nova consiga entender um domínio sem precisar conhecer o projeto inteiro",[320,1578,1579],{},"Componentes, páginas, stores e composables de uma mesma funcionalidade estão ficando espalhados demais",[1559,1581,1583],{"id":1582},"podem-ser-overkill-quando","Podem ser overkill quando",[317,1585,1586,1589,1592,1595,1598],{},[320,1587,1588],{},"O projeto é um MVP ou protótipo e velocidade importa mais que estrutura",[320,1590,1591],{},"Você tem poucos componentes e tudo ainda cabe facilmente na estrutura padrão",[320,1593,1594],{},"É um site institucional ou landing page sem domínios de negócio reais",[320,1596,1597],{},"O time é pequeno, o prazo é curto e não existe previsão de crescimento",[320,1599,1600],{},"A separação criaria mais configuração do que benefício real",[11,1602,1603],{},"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.",[31,1605],{},[34,1607,1609],{"id":1608},"a-pergunta-que-guiou-a-decisão","A pergunta que guiou a decisão",[11,1611,1612],{},"A mesma pergunta do artigo anterior continua válida aqui:",[11,1614,1615],{},[23,1616,1617],{},"Se eu precisar mudar isso daqui a seis meses, qual é o menor número de arquivos que vou precisar tocar?",[11,1619,1620],{},"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.",[11,1622,1623],{},"No meu caso, Nuxt Layers ajudou a organizar melhor essas responsabilidades antes que a estrutura inicial trouxesse uma limitação.",[31,1625],{},[34,1627,1629],{"id":1628},"leituras-relacionadas","Leituras relacionadas",[317,1631,1632,1639,1646,1652],{},[320,1633,1634],{},[15,1635,1638],{"href":1636,"rel":1637},"https:\u002F\u002Flarisantos.vercel.app\u002Fblog\u002Fnuxt-saas-multitenant",[19],"Como estruturei a arquitetura inicial de um SaaS multitenant com Nuxt 3 e Supabase",[320,1640,1641],{},[15,1642,1645],{"href":1643,"rel":1644},"https:\u002F\u002Flarisantos.vercel.app\u002Fblog\u002Fprincipio-solid",[19],"SOLID: 5 Princípios para Escrever Código Limpo e Escalável",[320,1647,1648],{},[15,1649,1651],{"href":17,"rel":1650},[19],"Arquitetura Modular com Nuxt Layers",[320,1653,1654],{},[15,1655,1658],{"href":1656,"rel":1657},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Flayers",[19],"Documentação oficial Nuxt Layers",[1660,1661,1662],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s3Er8, html code.shiki .s3Er8{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sK_r7, html code.shiki .sK_r7{--shiki-light:#6182B8;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sMo7A, html code.shiki .sMo7A{--shiki-light:#90A4AE;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sG-J9, html code.shiki .sG-J9{--shiki-light:#39ADB5;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sdv8B, html code.shiki .sdv8B{--shiki-light:#E53935;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sF_wb, html code.shiki .sF_wb{--shiki-light:#39ADB5;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s0vBq, html code.shiki .s0vBq{--shiki-light:#91B859;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sk1zL, html code.shiki .sk1zL{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sFsEu, html code.shiki .sFsEu{--shiki-light:#9C3EDA;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFfmW, html code.shiki .sFfmW{--shiki-light:#39ADB5;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sqIbZ, html code.shiki .sqIbZ{--shiki-light:#E53935;--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .s7047, html code.shiki .s7047{--shiki-light:#9C3EDA;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .soiBB, html code.shiki .soiBB{--shiki-light:#E2931D;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sSJ72, html code.shiki .sSJ72{--shiki-light:#91B859;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":47,"searchDepth":61,"depth":61,"links":1664},[1665,1666,1667,1668,1669,1670,1671,1675,1676],{"id":36,"depth":61,"text":37},{"id":304,"depth":61,"text":305},{"id":348,"depth":61,"text":349},{"id":686,"depth":61,"text":687},{"id":1246,"depth":61,"text":1247},{"id":1524,"depth":61,"text":1525},{"id":1553,"depth":61,"text":1554,"children":1672},[1673,1674],{"id":1561,"depth":67,"text":1562},{"id":1582,"depth":67,"text":1583},{"id":1608,"depth":61,"text":1609},{"id":1628,"depth":61,"text":1629},"2026-06-15T00:17:35-03:00","Um relato prático sobre como migrei um projeto SaaS multitenant para arquitetura modular com Nuxt Layers: as decisões, os ajustes e o raciocínio por trás da nova estrutura.","md","\u002Fimages\u002Fblog\u002Fblog-nuxt-layers.png",{},"\u002Fblog\u002Fsass-nuxt-layers",{"title":5,"description":1678},"blog\u002Fsass-nuxt-layers",[1686,1325,1687,1688,1689,1690,1691],"nuxt","arquitetura","layers","multitenant","typescript","saas","bMGTOShdAPDmh0wzDFCeafTNl-b1xHbGAfXwsbV5ytI",1782501671031]