
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...
lenne.tech Frontend Development
Gotchas
- The current
nuxt-base-starterruns vitest+oxlint+oxfmt, not eslint+prettier — Projects originally generated from older starters still shipeslint+prettier+ Playwright-only. When aligning a project with the current starter, expect to migrate the entire toolchain: installoxlint+oxfmt+vitest+@vitejs/plugin-vue+happy-dom, dropeslint+prettier+jsdom, addtests/unit/setup.ts+tests/unit/mocks/nuxt-imports.ts, and updatepackage.jsonscripts totest:unit,test:e2e,lint,format,format:check, plus thecheck/check:fixaggregate. Full recipe lives in themodernizing-toolchainskill (Phase 4). - Nuxt's PORT vs NITRO_PORT — Some Nitro versions read
process.env.PORTas a string and feed it directly intonet.Server#listen, which crashes withERR_SOCKET_BAD_PORT options.port should be >= 0 and < 65536. Received type string. Always preferNITRO_PORT=<num>for the production build (node .output/server/index.mjs) —NITRO_PORTis 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.tsdevServer.portworks 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-starterbaseline, sync every dep version (bothdependenciesanddevDependencies) 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: whenRollup 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 inpackage.json, even if no app code imports it directly. pnpm run generate-typesneeds 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_URLfrom the shell (a.envfile is not read), else<repo-root>/.lt-dev/.env(written bylt dev up, which also carries theNODE_EXTRA_CA_CERTSthe Caddy HTTPS host needs), else a hard exit 1 with an actionable message. It additionally refuses a URL belonging to a differentlt devproject. So underlt dev upthe 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 oldhttp://localhost:3000default is exactly the bug: on a machine with parallel worktrees that port belongs to whichever project holds it, and the generator then wrotetypes.gen.ts/sdk.gen.tsfrom 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, orcurl http://localhost:3000/healthin classic mode) and check that an expected endpoint really appears insdk.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*.vuepages/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.duvsSietone 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 isuseOverlay().create(ModalComponent)from composables. Seereference/modals.md. types.gen.tsandsdk.gen.tsare GENERATED — never hand-edit — Manual changes are overwritten on nextgenerate-typesrun. If a type is missing, the fix is on the API side (add@ApiProperty,@Field, etc.) not in the generated file..gitignoredoes 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 upis 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. Runlt dev initonce to migrate. Seemanaging-dev-serversskill 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) orpnpm 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/orpackages/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.md | Start 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 setupAUTH.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.tsfrom the start - Always use generated types from
types.gen.ts(never manual interfaces for DTOs) - Run
pnpm run generate-typeswith 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 Intent | Correct 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 initializationbuilding-stories-with-tdd- For complete TDD workflow (Backend + Frontend)contributing-to-lt-framework- When modifying@lenne.tech/nuxt-extensionsitself and testing viapnpm link/lt-dev:frontend:env-migrate- Migrate env variables toNUXT_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/orpackages/app/→ This skillprojects/api/orpackages/api/→generating-nest-serversskill
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
| Priority | Source | Use For |
|---|---|---|
| 1. | ~/api-client/types.gen.ts | All backend DTOs (REQUIRED) |
| 2. | ~/api-client/sdk.gen.ts | All API calls (REQUIRED) |
| 3. | Nuxt UI types | Component props (auto-imported) |
| 4. | app/interfaces/*.interface.ts | Frontend-only types (UI state, forms) |
Standards
| Rule | Value |
|---|---|
| UI Labels | Match the project's language — detect, never assume (see Gotchas) |
| Code/Comments | English |
| Styling | TailwindCSS only, no <style> |
| Colors | Semantic only (primary, error, success) |
| Types | Explicit, no implicit any |
| Backend Types | Generated only (types.gen.ts) |
| Composables | app/composables/use*.ts |
| Shared State | useState() for SSR-safe state |
| Local State | ref() / reactive() |
| Forms | Valibot (not Zod) |
| Modals | useOverlay() |
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 readspackage.jsonversion +process.env.APP_VERSION_COMMIT). - API build is fetched from the public
GET /metaviabuildLtApiUrl('/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) beforenuxt build, becauseruntimeConfig.publicis 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 rawerror.messagein 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 viauseState) - 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 fromtranslateError - 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
| Topic | File |
|---|---|
| Core Patterns | reference/patterns.md |
| Service Health Check | reference/service-health-check.md |
| Browser Testing | reference/browser-testing.md |
| TypeScript | reference/typescript.md |
| Components | reference/components.md |
| Composables | reference/composables.md |
| Forms | reference/forms.md |
| Modals | reference/modals.md |
| API | reference/api.md |
| Colors | reference/colors.md |
| Nuxt Patterns | reference/nuxt.md |
| Authentication | reference/authentication.md |
| E2E Testing | reference/e2e-testing.md |
| Troubleshooting | reference/troubleshooting.md |
| Security | reference/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 fromtypes.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 usemiddleware: 'auth' - AI chat uses
useLtAiChat().stop()for clean abort (NEVER rawAbortController.abort()on auseLtAi*stream — the composable does cleanup and treats AbortError as a clean stop) -
LtAiPromptInput(CRUD foruseLtAiPrompts) vsLtAiPromptRunInput(execution payload foruseLtAi.prompt()/.promptStream()) — never conflate; pre-1.7.0 they collided as one name and TypeScript silently merged them - No
v-htmlwith user content, tokens stored securely - All error-handling sites route through
useLtErrorTranslation()— no raw backend messages in Toasts / UI - Security review passed (
/lt-dev:reviewfor general scan) - Feature tested in browser (Chrome DevTools MCP), no console errors