lenneTech avatar

developing-lt-frontend

PRIMARY expert for ALL Nuxt and Vue frontend tasks. ALWAYS use this skill when working with Nuxt 4, Vue components, Nuxt UI, frontend pages, or files in app/components/, app/composables/, app/pages/, app/interfaces/ (supports monorepos with projects/app/, packages/app/). Handles modals (useOverla...

提供方 lenneTech|开源

lenne.tech Frontend Development

Gotchas

  • The current nuxt-base-starter runs vitest+oxlint+oxfmt, not eslint+prettier — Projects originally generated from older starters still ship eslint + prettier + Playwright-only. When aligning a project with the current starter, expect to migrate the entire toolchain: install oxlint + oxfmt + vitest + @vitejs/plugin-vue + happy-dom, drop eslint + prettier + jsdom, add tests/unit/setup.ts + tests/unit/mocks/nuxt-imports.ts, and update package.json scripts to test:unit, test:e2e, lint, format, format:check, plus the check / check:fix aggregate. Full recipe lives in the modernizing-toolchain skill (Phase 4).
  • Nuxt's PORT vs NITRO_PORT — Some Nitro versions read process.env.PORT as a string and feed it directly into net.Server#listen, which crashes with ERR_SOCKET_BAD_PORT options.port should be >= 0 and < 65536. Received type string. Always prefer NITRO_PORT=<num> for the production build (node .output/server/index.mjs) — NITRO_PORT is the documented Nitro-specific knob, goes through Nitro's own env loader, and is coerced to number reliably. The Nuxt dev server (nuxt dev) is unaffected — nuxt.config.ts devServer.port works as expected.
  • Aligning with the upstream starter is a wholesale dep sync, not a curated pick — When the project is being brought to the current nuxt-base-starter baseline, sync every dep version (both dependencies and devDependencies) to what the starter ships and read the CHANGELOG of any package whose major moved. The recurring trap that a blanket version-sync does not fix is missing direct deps after a peer-restructure: when Rollup failed to resolve import "X" (or an equivalent module-not-found at install time) appears, the wrapper package no longer pulls "X" transitively — declare X as a direct dependency in package.json, even if no app code imports it directly.
  • pnpm run generate-types needs a RUNNING API — and on the current starter it refuses to guess which one. The generator fetches the OpenAPI schema from the API. Since DEV-2802 the starter resolves that URL instead of defaulting: NUXT_API_URL from the shell (a .env file is not read), else <repo-root>/.lt-dev/.env (written by lt dev up, which also carries the NODE_EXTRA_CA_CERTS the Caddy HTTPS host needs), else a hard exit 1 with an actionable message. It additionally refuses a URL belonging to a different lt dev project. So under lt dev up the documented call needs no extra env, and a stale or foreign API can no longer be generated from silently. Do not "fix" that hard failure by re-adding a fallback — the old http://localhost:3000 default is exactly the bug: on a machine with parallel worktrees that port belongs to whichever project holds it, and the generator then wrote types.gen.ts / sdk.gen.ts from a foreign contract, reported success and exited 0. Older projects that have not adopted the guard still carry that silent fallback; there, verify the API is up before regenerating (curl -k https://api.<slug>.localhost/health, or curl http://localhost:3000/health in classic mode) and check that an expected endpoint really appears in sdk.gen.ts.
  • UI language: detect it from the project — NEVER assume German. UI text (labels, buttons, placeholders, toasts) must match the language the project already uses. Determine it in this order: (1) an explicit project rule wins — check the project's CLAUDE.md, a conventions doc, or i18n config; (2) otherwise infer from existing UI files — match the language already used across *.vue pages/components; (3) only default to German for a true greenfield with no rule and no existing UI text. NEVER bulk-translate an existing app from one language to another — silently flipping an established English UI to German (or vice-versa) is a destructive, review-failing change that has broken a whole project before. The project — not this plugin — decides the language; once detected, stay consistent with it (incl. du vs Sie tone for German).
  • Use useOverlay() for modals — NOT conditional rendering — The default instinct is <MyModal v-if="showModal" />. This bypasses Nuxt UI's modal stack, breaks focus trapping, and causes z-index issues with nested dialogs. The correct pattern is useOverlay().create(ModalComponent) from composables. See reference/modals.md.
  • types.gen.ts and sdk.gen.ts are GENERATED — never hand-edit — Manual changes are overwritten on next generate-types run. If a type is missing, the fix is on the API side (add @ApiProperty, @Field, etc.) not in the generated file. .gitignore does NOT ignore these files — they ARE committed, but only via the regeneration command.
  • Better Auth derives its origins from BASE_URL/APP_URL — not from a hardcoded port number. When lt dev up is used, these env vars are set automatically to the project's stable HTTPS URLs (https://api.<slug>.localhost, https://<slug>.localhost) and auth works regardless of internal port. The legacy "3000/3001 only" rule applies ONLY to non-migrated projects with hardcoded URLs. Run lt dev init once to migrate. See managing-dev-servers skill for the full URL rules.
  • Limit local Playwright runs to new + affected specs to keep TDD loops fast — The full Playwright suite is slow and runs in CI. During local development / TDD, default to running only the new + affected specs via lt dev test -- <spec> (lt-projects) or pnpm dlx playwright test <spec> (non-lt). Backend Unit + API stay unrestricted — they're fast and catch cross-pillar regressions. Only run the full local Playwright suite when the user explicitly asks. Full recipe: reference/e2e-testing.md → "Local TDD Loop — Affected Specs Only".

Ecosystem Context

Developers typically work in a Lerna fullstack monorepo created via lt fullstack init:

project/
├── projects/
│   ├── api/    ← nest-server-starter (depends on @lenne.tech/nest-server)
│   └── app/    ← nuxt-base-starter (depends on @lenne.tech/nuxt-extensions)
├── lerna.json
└── package.json (workspaces: ["projects/*"])

Package relationships:

  • nuxt-base-starter (template) → depends on @lenne.tech/nuxt-extensions (plugin)
  • @lenne.tech/nuxt-extensions provides pre-built composables, components, and types aligned with @lenne.tech/nest-server
  • This skill covers projects/app/ and any code using nuxt-base-starter or nuxt-extensions

When to Use This Skill

  • Working with Nuxt 4 projects (nuxt.config.ts present)
  • Editing files in app/components/, app/composables/, app/pages/, app/interfaces/
  • Creating or modifying Vue components with Nuxt UI
  • Integrating backend APIs via generated types (types.gen.ts, sdk.gen.ts)
  • Building forms with Valibot validation
  • Implementing authentication (login, register, 2FA, passkeys)
  • Working in monorepos with projects/app/ or packages/app/ structure

NOT for: NestJS backend development (use generating-nest-servers skill instead)

Framework Source Files (MUST READ before guessing)

ALWAYS read actual source code from node_modules/@lenne.tech/nuxt-extensions/ before guessing framework behavior. The framework ships documentation with the npm package.

File (in node_modules/@lenne.tech/nuxt-extensions/)When to Read
CLAUDE.mdStart of any frontend task — composables, components, config
dist/runtime/composables/Available composables (useLtAuth, useLtAuthClient, useLtTusUpload, useLtFile, useLtShare, useLtErrorTranslation, and from 1.7.0 the useLtAi* family)
dist/runtime/components/Available components
dist/runtime/utils/Available utilities
dist/runtime/types/TypeScript type definitions

Also read the nuxt-base-starter documentation:

  • README.md — Project overview, tech stack, auth setup
  • AUTH.md — Better Auth integration details

CRITICAL: Real Backend Integration FIRST

Never use placeholder data, TODO comments, or manual interfaces!

  • Always use real API calls via sdk.gen.ts from the start
  • Always use generated types from types.gen.ts (never manual interfaces for DTOs)
  • Run pnpm run generate-types with API running before starting frontend work
  • Implement feature-by-feature with full backend integration

Before starting: Ensure services are running. See reference/service-health-check.md

Skill Boundaries

User IntentCorrect Skill
"Build a Vue component"THIS SKILL
"Create a Nuxt page"THIS SKILL
"Style with TailwindCSS"THIS SKILL
"Create a NestJS module"generating-nest-servers
"Security audit of frontend"general-frontend-security
"Implement with TDD"building-stories-with-tdd

Related Skills

Works closely with:

  • generating-nest-servers - For NestJS backend development (projects/api/)
  • using-lt-cli - For Git operations and Fullstack initialization
  • building-stories-with-tdd - For complete TDD workflow (Backend + Frontend)
  • contributing-to-lt-framework - When modifying @lenne.tech/nuxt-extensions itself and testing via pnpm link
  • /lt-dev:frontend:env-migrate - Migrate env variables to NUXT_ prefix convention

Dev Server Lifecycle

When starting the App for manual testing, Chrome DevTools MCP debugging, or E2E tests: prefer lt dev up over nuxt dev directly. It serves the App under a stable HTTPS URL (https://<slug>.localhost) via Caddy, sets NUXT_API_URL/NUXT_PUBLIC_SITE_URL/NUXT_PUBLIC_STORAGE_PREFIX/NUXT_PUBLIC_API_PROXY=false automatically, and detaches into <root>/.lt-dev/app.log. Stop with lt dev down. For non-lt-projects (or when explicitly requested): use run_in_background: true and pkill -f "nuxt dev" afterwards. Leaving dev servers orphaned blocks the Claude Code session ("Unfurling..."). Full rules: managing-dev-servers skill.

In monorepo projects:

  • projects/app/ or packages/app/This skill
  • projects/api/ or packages/api/generating-nest-servers skill

Nuxt 4 Directory Structure

app/                  # Application code (srcDir)
├── components/       # Auto-imported components
├── composables/      # Auto-imported composables
├── interfaces/       # TypeScript interfaces
├── lib/              # Utility libraries (auth-client, etc.)
├── pages/            # File-based routing
├── layouts/          # Layout components
├── utils/            # Auto-imported utilities
└── api-client/       # Generated types & SDK
server/               # Nitro server routes
public/               # Static assets
nuxt.config.ts

Type Rules

PrioritySourceUse For
1.~/api-client/types.gen.tsAll backend DTOs (REQUIRED)
2.~/api-client/sdk.gen.tsAll API calls (REQUIRED)
3.Nuxt UI typesComponent props (auto-imported)
4.app/interfaces/*.interface.tsFrontend-only types (UI state, forms)

Standards

RuleValue
UI LabelsMatch the project's language — detect, never assume (see Gotchas)
Code/CommentsEnglish
StylingTailwindCSS only, no <style>
ColorsSemantic only (primary, error, success)
TypesExplicit, no implicit any
Backend TypesGenerated only (types.gen.ts)
Composablesapp/composables/use*.ts
Shared StateuseState() for SSR-safe state
Local Stateref() / reactive()
FormsValibot (not Zod)
ModalsuseOverlay()

Build Identity / Drift Detection

The starter ships /app/admin/system + useSystem() to show which build runs and detect a drifted / stale deployment (App vs. API on different commits):

  • App build is baked at build time into runtimeConfig.public.appVersion / appCommit (nuxt.config reads package.json version + process.env.APP_VERSION_COMMIT).
  • API build is fetched from the public GET /meta via buildLtApiUrl('/meta') (auto-imported, SSR/proxy-aware) — never hardcode the API URL.
  • Compare by commit only (buildsMatch); version numbers are per-component and may legitimately differ. 'unknown' commits never trigger the warning.
  • The Docker build must pass APP_VERSION_COMMIT (= CI commit SHA) before nuxt build, because runtimeConfig.public is frozen at build time.

TDD for Frontend

1. Backend API must be complete (API tests pass)
2. Write E2E tests BEFORE implementing frontend
3. Implement components/pages until E2E tests pass
4. Debug with Chrome DevTools MCP

Complete E2E testing guide: reference/e2e-testing.md

Error Handling — Consume Backend ErrorCodes via useLtErrorTranslation

The backend returns structured errors in the format #LTNS_XXXX: Developer message (core) or #PROJ_XXXX: ... (project-specific). The @lenne.tech/nuxt-extensions package ships useLtErrorTranslation() which parses the #CODE: marker, loads locale-specific translations from GET /i18n/errors/:locale, and returns end-user messages.

NEVER assert or display raw English backend messages in the UI. Always pipe errors through translateError() / showErrorToast() so users see localized text.

<script setup lang="ts">
const { translateError, showErrorToast, parseError } = useLtErrorTranslation();
const toast = useToast();

async function onSubmit() {
  try {
    await $fetch('/api/users', { method: 'POST', body: form.value });
  } catch (error) {
    // Preferred — direct toast from translated message
    showErrorToast(error, 'Speichern fehlgeschlagen');

    // Or manual, if you need more control
    toast.add({
      color: 'error',
      title: 'Speichern fehlgeschlagen',
      description: translateError(error),  // '#LTNS_0400: Resource not found' → 'Ressource nicht gefunden.'
    });

    // Or parse for custom handling (e.g. redirect on specific code)
    const parsed = parseError(error);
    if (parsed.code === 'LTNS_0023') {
      await navigateTo('/auth/verify-email');
    }
  }
}
</script>

Rules:

  • Every error-handling site uses useLtErrorTranslation() — no raw error.message in Toast descriptions, form errors, or page-level error UI
  • loadTranslations(locale) is called once at app start or on locale change (the composable caches per locale via useState)
  • Code-based branching (if (parsed.code === 'LTNS_XXXX')) for flow-control decisions (verification-required redirects, retry prompts) — never branch on message-string contents
  • Toast titles are hardcoded in the project's UI language (context-specific, e.g. German 'Anmeldung fehlgeschlagen' or the project's English equivalent); descriptions come from translateError
  • Tests assert translated messages (not English error.message) — see the test-reviewer rules in this plugin

Full consumer reference: reference/error-translation.md

Reference Files

TopicFile
Core Patternsreference/patterns.md
Service Health Checkreference/service-health-check.md
Browser Testingreference/browser-testing.md
TypeScriptreference/typescript.md
Componentsreference/components.md
Composablesreference/composables.md
Formsreference/forms.md
Modalsreference/modals.md
APIreference/api.md
Colorsreference/colors.md
Nuxt Patternsreference/nuxt.md
Authenticationreference/authentication.md
E2E Testingreference/e2e-testing.md
Troubleshootingreference/troubleshooting.md
Securityreference/security.md
Error Translation (consume backend ErrorCodes)reference/error-translation.md
Informed Trade-offs (Composition API, readonly, SSR guards, v-html, useFetch)reference/informed-trade-off-pattern.md

Pre-Commit Checklist

  • No placeholder data, no TODO comments for API
  • All API calls via sdk.gen.ts, all types from types.gen.ts
  • Logic in composables, modals use useOverlay, forms use Valibot
  • TailwindCSS only, semantic colors only
  • UI text matches the project's detected language (not assumed German), code/comments English, no implicit any
  • Auth uses useLtAuth(), protected routes use middleware: 'auth'
  • AI chat uses useLtAiChat().stop() for clean abort (NEVER raw AbortController.abort() on a useLtAi* stream — the composable does cleanup and treats AbortError as a clean stop)
  • LtAiPromptInput (CRUD for useLtAiPrompts) vs LtAiPromptRunInput (execution payload for useLtAi.prompt() / .promptStream()) — never conflate; pre-1.7.0 they collided as one name and TypeScript silently merged them
  • No v-html with user content, tokens stored securely
  • All error-handling sites route through useLtErrorTranslation() — no raw backend messages in Toasts / UI
  • Security review passed (/lt-dev:review for general scan)
  • Feature tested in browser (Chrome DevTools MCP), no console errors
developing-lt-frontend - 适用于 Claude Code 与 Cursor 的 AI 智能体 Skill | Agent Skills