Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Nuxt Base

Template Nuxt 4 que serve de ponto de partida para qualquer projeto: dashboard atrás de login, site com SEO, SaaS full-stack ou frontend puro contra API externa. A base é agnóstica (não impõe backend, banco nem auth — deixa os pontos de encaixe prontos), enxuta (na dúvida, fica de fora com receita documentada) e copy-and-own na UI (o visual é nosso; bibliotecas estilizadas não entram como dependência). A especificação completa está em SPEC.md.

Requisitos

  • Node 22.19+ (ou 24.11+ — o CI usa Node 24)
  • pnpm 11 — a versão exata está pinada em packageManager no package.json

Comandos

pnpm install # instala dependências e roda nuxt prepare
pnpm dev # servidor de desenvolvimento em http://localhost:3000
pnpm build # build de produção (.output/)
pnpm preview # serve o build localmente
pnpm lint # ESLint (inclui formatação — não há Prettier)
pnpm lint:fix # ESLint com autofix
pnpm typecheck # vue-tsc via nuxt typecheck
pnpm test# Vitest (uma execução)
pnpm test:watch # Vitest em modo watch

Estrutura de pastas

app/ # código da aplicação (srcDir do Nuxt 4)
assets/css/main.css # design tokens (Tailwind 4 CSS-first) — identidade visual vive aqui
components/ui/ # kit próprio: Button, Input, Select, Modal, Card, Badge, Table, Toaster, Tooltip
composables/ # useApi/useApiData (porta única para a API), useToast
layouts/default.vue # header + nav + toggle de tema + <UiToaster />
middleware/auth.ts # esqueleto do middleware de rota (auth é ponto de encaixe)
pages/ # index, login (placeholder), components (vitrine do kit)
stores/app.ts # store-referência Pinia (setup store)
error.vue # página de erro global + 404
server/
api/health.get.ts # rota-referência do Nitro (GET /api/health)
tests/nuxt/ # specs aqui entram no nuxt typecheck
components/ # referência (mountSuspended) + smoke de regressão do kit
composables/ # teste-referência de composable (registerEndpoint)
conventions/ # teste-inventário: só tokens semânticos
.claude/skills/ # skills de IA (preline-mcp, novo-componente-ui, derivar-projeto, ci-verde)
.env.example # espelho documentado do runtimeConfig
nuxt.config.ts # módulos, css, runtimeConfig
vitest.config.ts # ambiente nuxt global + happy-dom
CLAUDE.md # convenções para sessões de IA
docs/SPEC.md # especificação da base

Como virar um projeto novo

TL;DR — do zero ao codando:

gh repo create meu-projeto --template DevJanderson/nuxt-base --private --clone
cd meu-projeto
pnpm install # deps + git hooks (lefthook) sozinho
claude # e dentro da sessão: /derivar-projeto meu-projeto

Virar um projeto novo = renomear + editar tokens + limpar exemplos (docs/SPEC.md §11). Três caminhos, em ordem de preferência:

  1. "Use this template" + skill (recomendado) — crie o repositório pelo botão do GitHub (ou gh repo create <nome> --template DevJanderson/nuxt-base --clone) e, no clone, invoque a skill /derivar-projeto <nome> no Claude Code: ela executa o roteiro completo e valida os gates.
  2. "Use this template" + passos manuais — mesmo início; siga os passos abaixo pulando o item 1 (o histórico já nasce limpo e os hooks se instalam no pnpm install).
  3. Cópia manual — copie a pasta e rode rm -rf .git && git init; zerar o .git apaga os git hooks, então rode também pnpm exec lefthook install.

Os passos (a fonte canônica, sempre atualizada, é a skill em .claude/skills/derivar-projeto/):

  1. Histórico e hooks — só no caminho 3, acima.
  2. Renomeie em dois lugares: package.json → campo "name"; server/api/health.get.ts → campo service (o healthcheck reporta o nome do serviço).
  3. Tokens em app/assets/css/main.css, apenas os blocos :root e .dark (e, se quiser, --font-sans/--radius-*) — ver Tema. Sem identidade definida ainda? Mantenha o padrão e siga: trocar depois é editar só esses dois blocos.
  4. Renderização: site/SEO mantém o SSR padrão; dashboard atrás de login → ssr: false; misto → routeRules — ver SSR ou SPA.
  5. Auth, quatro ramos: receita 1 ou 2; sem login → remova app/middleware/auth.ts, app/pages/login.vue e o redirect de 401 no useApi; login futuro → mantenha os pontos de encaixe como estão e não instale nada.
  6. Limpe os exemplos: vitrine /components (mantê-la como styleguide interno é válido; se remover, tire o link do header), app/pages/index.vue, marca no app/layouts/default.vue, app/stores/app.ts. Ao final, caça-marca: grep -ri "nuxt base" app/ server/ — a marca vive também em error.vue e nos useSeoMeta; zere o resultado.
  7. Docs e ambiente: título/descrição de README e CLAUDE.md (as convenções continuam valendo), remova esta seção (já cumprida), .env.example só com as variáveis reais do projeto (fora as das receitas não adotadas) e copie para .env.
  8. Valide: pnpm install && pnpm dev sem erro e sem warning — se a porta 3000 estiver ocupada o Nuxt escolhe outra, confira no log — e /api/health reportando o nome novo; pnpm verify inteiro verde. Feche com o commit inicial.

A suíte herdada continua valendo no projeto novo: os testes de referência (componente e composable), o smoke de regressão do kit e o teste-inventário de tokens.

Tema (identidade visual)

Os tokens vivem em app/assets/css/main.css, em duas camadas:

  • Camada 1:root (tema claro) e .dark (tema escuro) definem os valores de cada token semântico. Trocar a identidade visual do projeto = editar só esses dois blocos.
  • Camada 2@theme inline registra os tokens como utilities do Tailwind (bg-primary, text-foreground, rounded-box, …). Só é editada para criar token novo.
TokenPapel
background / foregroundfundo e texto da página
card / card-foregroundsuperfícies elevadas (cards, modais)
muted / muted-foregroundfundos discretos e texto secundário
borderbordas e divisores
primary / primary-hover / primary-foregroundação principal
destructive / destructive-foregroundações destrutivas e erros
ringanel de foco
--radius-boxrounded-boxraio de containers (cards, modais)
--radius-fieldrounded-fieldraio de controles (botões, inputs)
--font-sanstipografia base

Regra da casa: componentes e páginas usam apenas tokens semânticos — nunca cor bruta (bg-blue-600, hex). Se precisar de uma cor nova, crie um token.

O dark mode é do @nuxtjs/color-mode (classe dark no <html>, preferência persistida). O tema claro é o padrão da plataforma (preference: 'light' no nuxt.config.ts — primeira visita abre clara independentemente do SO); o toggle está no layout default e a escolha do usuário persiste: colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'. Em SSR, renderize ícones dependentes do tema dentro de <ClientOnly> (o valor efetivo só é conhecido no client).

SSR ou SPA

A base vem com SSR ligado (bom para SEO e primeira pintura). Ajuste por tipo de projeto no nuxt.config.ts:

// Dashboard/painel 100% atrás de login → SPA globalexportdefaultdefineNuxtConfig({ssr: false,})
// Misto: só a área logada vira SPA; o resto continua SSRexportdefaultdefineNuxtConfig({routeRules: {'/app/**': {ssr: false},// área logada renderiza só no client'/': {prerender: true},// landing gerada no build},})

Site com SEO: mantenha o padrão da base e acrescente prerender/swr nas rotas que couber.

API e dados

Toda chamada HTTP sai por app/composables/useApi.ts:

  • useApi() → instância configurada de $fetch para chamadas imperativas (handlers de evento, stores, middleware):

    constapi=useApi()constuser=awaitapi<User>('/users/42')awaitapi('/users/42',{method: 'PATCH',body: {name: 'Ana'}})
  • useApiData<T>(url, options?) → wrapper de useFetch com a mesma instância, para data fetching SSR-friendly no setup de componentes:

    const{data: users, status, error }=awaituseApiData<User[]>('/users')

Comportamento embutido:

  • baseURL vem de runtimeConfig.public.apiBase (default /api, o Nitro do próprio app). Para consumir API externa, defina NUXT_PUBLIC_API_BASE=https://api.exemplo.com no .env.
  • Credencial: se o cookie auth.token tiver valor, toda chamada sai com Authorization: Bearer <token> (ver receita 2 de auth). Sem cookie, nenhum header é enviado.
  • Erros padronizados: respostas de erro rejeitam com createError no formato ApiError{ statusCode, statusMessage, data }.
  • 401 no client redireciona para /login (remova ou ajuste no useApi se o projeto não usar esse fluxo).

Healthcheck de referência: GET /api/health{ status: 'ok', service: 'nuxt-base', timestamp }. Novos endpoints Nitro seguem o formato de server/api/health.get.ts (arquivo <nome>.<método>.ts + retorno tipado).

Auth: duas receitas

A base não implementa autenticação — entrega o middleware auth (esqueleto em app/middleware/auth.ts), a página /login placeholder e o useApi preparado para injetar credencial. Proteja páginas com definePageMeta({ middleware: 'auth' }) e escolha uma receita:

Receita 1 — sessão no servidor (SaaS full-stack) com nuxt-auth-utils

Sessão em cookie selado/criptografado, gerenciada pelo próprio Nitro. Recomendação oficial do time Nuxt.

npx nuxi module add auth-utils

Defina NUXT_SESSION_PASSWORD no .env (mínimo 32 caracteres; em dev é gerada automaticamente no primeiro nuxt dev, em produção é obrigatória — já documentada no .env.example).

// server/api/auth/login.post.tsexportdefaultdefineEventHandler(async(event)=>{const{ email, password }=awaitreadBody(event)constuser=awaitverifyCredentials(email,password)// sua validação (banco, serviço, …)awaitsetUserSession(event,{user: {email: user.email}})return{ok: true}})

No client, useUserSession() expõe { loggedIn, user, session, clear }. Em app/middleware/auth.ts, descomente o bloco da receita 1:

const{ loggedIn }=useUserSession()if(!loggedIn.value){returnnavigateTo('/login')}

Em rotas de API protegidas, use await requireUserSession(event) no início do handler.

Receita 2 — token contra API externa (frontend puro)

O login troca credenciais por um token na API externa e o grava no cookie auth.token — o mesmo que o useApi já lê para injetar o Bearer.

Aponte a base para a API externa no .env: NUXT_PUBLIC_API_BASE=https://api.exemplo.com.

// essência da página de loginconstemail=ref('')constpassword=ref('')consttoken=useCookie('auth.token',{maxAge: 60*60*24*7,sameSite: 'lax'})asyncfunctionsubmit(){constapi=useApi()constresponse=awaitapi<{token: string}>('/auth/login',{method: 'POST',body: {email: email.value,password: password.value},})token.value=response.tokenawaitnavigateTo('/')}

Em app/middleware/auth.ts, descomente o bloco da receita 2:

consttoken=useCookie('auth.token')if(!token.value){returnnavigateTo('/login')}

A partir daí toda chamada do useApi sai autenticada; um 401 da API derruba o usuário de volta para /login (comportamento já embutido). Logout = useCookie('auth.token').value = null.

Componentes de UI

O kit vive em app/components/ui/ e é auto-importado com prefixo Ui (<UiButton>, <UiModal>, …). A vitrine com todos os componentes em uso está em /components.

ComponenteProps essenciaisSlots
UiButtonvariant (solid | outline | ghost | destructive), size (sm | md | lg), disabled, typedefault
UiInputv-model, label, hint, error, type, placeholder, disabled
UiSelectv-model, items: { label, value, disabled? }[], label, placeholder, error, disabled
UiModalv-model:open, title (obrigatória), descriptiontrigger, default, footer
UiCardheader, default, footer
UiBadgevariant (neutral | primary | destructive | outline)default
UiTablecolumns: { key, label }[], rows#cell-[key] recebe { row, value }
UiToastermontado uma única vez no layout default

Toasts são imperativos, via composable: useToast() retorna { toasts, dismiss, success, error, info } — ex.: toast.success('Salvo.', { title: 'Pronto', duration: 8000 }).

Regra do copy-and-own (SPEC §2 e §4): o comportamento vem do Reka UI (headless — foco, teclado e ARIA resolvidos; esse sim é dependência) e o visual é markup portado do Preline UI, adaptado aos nossos tokens. Preline nunca entra como dependência — nem o pacote npm, nem o plugin JS; é catálogo de referência e fonte de cópia. Componente novo segue o mesmo caminho: escolher o primitivo Reka, portar o markup do Preline, traduzir variantes hs-* para os estados data-[state=…] do Reka e usar apenas tokens semânticos. Visual inspirado no Preline UI (MIT).

Testes

Stack: Vitest 4 + @nuxt/test-utils + happy-dom, com ambiente nuxt global (vitest.config.ts) — todo teste roda com o runtime do Nuxt (auto-imports, plugins, #components, #imports).

Dois testes de referência definem o padrão da casa — copie a estrutura deles:

  • Componentetests/nuxt/components/button.spec.ts: mountSuspended de @nuxt/test-utils/runtime + import do componente via #components. Cobre render de slot, classes por variante e estado disabled.
  • Composable/APItests/nuxt/composables/use-api.spec.ts: registerEndpoint de @nuxt/test-utils/runtime mocka rotas no Nitro de teste — sem rede real. Atenção: registre o caminho completo, incluindo o prefixo da baseURL (/api/ping, não /ping). Cobre resposta feliz tipada, erro padronizado e a injeção condicional do header Authorization.

Há ainda um teste-inventário (tests/nuxt/conventions/semantic-tokens.spec.ts) que trava a convenção "apenas tokens semânticos" em código: cor bruta nova em app/**/*.vue falha o CI, e entrada de allowlist que apodreceu também.

pnpm test# uma execução (é o que o CI roda)
pnpm test:watch # watch mode durante o desenvolvimento

Versionamento e branches

  • main única — o template não usa develop: o "Use this template" copia a branch default, e é ela que os gates mantêm sempre estável. Mudança arriscada = branch de feature ad hoc + PR (CI verde antes do merge), sem branch permanente.
  • Tags de versão (v1.0.0, …) marcam estados estáveis da base — um projeto derivado sabe de qual versão nasceu.
  • Projetos derivados decidem o próprio fluxo conforme a realidade de deploy (trunk-based, develop → main, preview environments) — a base não impõe.

CI, hooks e saúde do código

  • Hooks de git (local, via lefthook.yml): pre-commit aplica eslint --fix nos arquivos staged; pre-push roda pnpm verify — o espelho exato do CI. Instalados automaticamente no pnpm install (postinstall do lefthook); após zerar o .git, rode pnpm exec lefthook install.
  • pnpm verify: lint && typecheck && test && knip && dup num comando. Regra da casa: "deveria funcionar" não é terminado — rode antes de considerar qualquer tarefa pronta.
  • CI (.github/workflows/ci.yml): a cada push na main e em todo PR — pnpm install --frozen-lockfile, lint, typecheck, knip, dup, test e build, em Node 24 com TZ: UTC (teste sensível a data falha igual aqui e em produção).
  • Auditoria semanal (.github/workflows/security.yml): pnpm audit --prod como gate toda segunda; auditoria completa informativa.
  • Renovate (renovate.json + .github/workflows/renovate.yml): updates não-major agrupados às segundas; majors exigem aprovação no Dependency Dashboard; major do TypeScript bloqueado (pino em 6.x). Roda self-hosted via Actions (segunda 06:00 e manual via workflow_dispatch), com o secret RENOVATE_TOKEN (token do dono — PRs disparam o CI normalmente; sem app externo). O workflow do template também cobre os derivados listados em RENOVATE_REPOSITORIES.
  • Anti-duplicação e código morto: regras sonarjs no ESLint (funções/branches idênticos), knip (exports, arquivos e dependências sem uso — config em knip.jsonc) e jscpd (pnpm dup — detector de copy-paste em app/, server/ e tests/, config em .jscpd.json). O threshold é 0: a base parte de 0,00% de duplicação e qualquer clone novo falha o CI — subir o threshold num projeto derivado é decisão consciente, documentada no .jscpd.json.
  • Anti-vazamento de logs: no-console no ESLint — erro em server/** (log de servidor estruturado é decisão do derivado; receita: pino) e aviso em app/ (só console.warn/console.error liberados). No build de produção do client, console.log/info/debug/trace são removidos do bundle (terser pure_funcs, só em $production no nuxt.config.ts); warn/error sobrevivem de propósito.
  • Varredura de segredos: job gitleaks no CI varre o histórico inteiro a cada push/PR (chaves, tokens, senhas commitados por acidente). Segredo detectado = CI vermelho → remova, rotacione a credencial (ela já vazou no histórico) e reescreva o histórico se o repo for público. .env/.env.* são gitignorados; só .env.example (com placeholders) é versionado.

About

Base Nuxt 4 — template para iniciar qualquer projeto: Tailwind 4 com design tokens, kit UI próprio (Reka UI), useApi tipado, testes e CI prontos

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages