Handles ALL Nuxt 4 and Vue frontend work: components, composables, forms (Valibot), API integration (types.gen.ts, sdk.gen.ts), authentication (Better Auth), SSR, and Playwright E2E. Supports monorepos (projects/app/, packages/app/). Activates on .vue files, nuxt.config.ts, Nuxt UI, TailwindCSS, files under app/{components,composables,pages,interfaces,layouts}/, and on "generate types", "Valibot form", "useOverlay modal", "Playwright E2E", "Frontend bauen". NOT for NestJS backend (use generating-nest-servers). NOT for framework-agnostic security theory (use general-frontend-security).
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.tsdevServer.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.
Nothing is fetched from a foreign host at runtime — ever. No CDN scripts, no Google Fonts, no icon sets pulled over the wire, no web-worker files loaded from unpkg/jsdelivr. Every asset ships in the build or is served from our own host. This is not a preference: a PDF viewer that loaded its pdfjs-dist worker from https://unpkg.com/... meant (1) no PDFs at all whenever that CDN was slow, blocked by a corporate firewall, or down — surfacing as the useless "Setting up fake worker failed", and turning a 5-second test into an 18-minute hang; (2) a request leaving for a foreign CDN on every single view, which breaks the DE/DSGVO hosting promise these projects are sold on; (3) a third party able to serve different code tomorrow — a supply-chain hole in the middle of the product. Wire assets through the bundler instead: with Vite/Nuxt that is import workerUrl from "pkg/file.mjs?url". When adding any dependency that renders, decodes or workers in the browser, check whether it fetches something at runtime — grep -rnoE "https?://" app/ over the source, and check the browser's network tab against the built app for third-party hosts.
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:
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.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)
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 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 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
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 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) beforenuxt 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
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
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 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
1---2name: developing-lt-frontend3description: Handles ALL Nuxt 4 and Vue frontend work: components, composables, forms (Valibot), API integration (types.gen.ts, sdk.gen.ts), authentication (Better Auth), SSR, and Playwright E2E. Supports monorepos (projects/app/, packages/app/). Activates on .vue files, nuxt.config.ts, Nuxt UI, TailwindCSS, files under app/{components,composables,pages,interfaces,layouts}/, and on "generate types", "Valibot form", "useOverlay modal", "Playwright E2E", "Frontend bauen". NOT for NestJS backend (use generating-nest-servers). NOT for framework-agnostic security theory (use general-frontend-security).4---56# lenne.tech Frontend Development78## Gotchas910- **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).11- **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.12- **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.13- **`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`.14- **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).15- **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`.16- **`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.17- **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.18- **Nothing is fetched from a foreign host at runtime — ever.** No CDN scripts, no Google Fonts, no icon sets pulled over the wire, no web-worker files loaded from `unpkg`/`jsdelivr`. Every asset ships in the build or is served from our own host. This is not a preference: a PDF viewer that loaded its `pdfjs-dist` worker from `https://unpkg.com/...` meant (1) **no PDFs at all** whenever that CDN was slow, blocked by a corporate firewall, or down — surfacing as the useless "Setting up fake worker failed", and turning a 5-second test into an 18-minute hang; (2) a request leaving for a foreign CDN on **every single view**, which breaks the DE/DSGVO hosting promise these projects are sold on; (3) a third party able to serve different code tomorrow — a supply-chain hole in the middle of the product. Wire assets through the bundler instead: with Vite/Nuxt that is `import workerUrl from "pkg/file.mjs?url"`. When adding any dependency that renders, decodes or workers in the browser, check whether it fetches something at runtime — `grep -rnoE "https?://" app/` over the source, and check the browser's network tab against the built app for third-party hosts.19- **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".2021## Ecosystem Context2223Developers typically work in a **Lerna fullstack monorepo** created via `lt fullstack init`:2425```26project/27├── projects/28│ ├── api/ ← nest-server-starter (depends on @lenne.tech/nest-server)29│ └── app/ ← nuxt-base-starter (depends on @lenne.tech/nuxt-extensions)30├── lerna.json31└── package.json (workspaces: ["projects/*"])32```3334**Package relationships:**35- **nuxt-base-starter** (template) → depends on **@lenne.tech/nuxt-extensions** (plugin)36- **@lenne.tech/nuxt-extensions** provides pre-built composables, components, and types aligned with `@lenne.tech/nest-server`37- This skill covers `projects/app/` and any code using nuxt-base-starter or nuxt-extensions3839## When to Use This Skill4041- Working with Nuxt 4 projects (nuxt.config.ts present)42- Editing files in `app/components/`, `app/composables/`, `app/pages/`, `app/interfaces/`43- Creating or modifying Vue components with Nuxt UI44- Integrating backend APIs via generated types (`types.gen.ts`, `sdk.gen.ts`)45- Building forms with Valibot validation46- Implementing authentication (login, register, 2FA, passkeys)47- Working in monorepos with `projects/app/` or `packages/app/` structure4849**NOT for:** NestJS backend development (use `generating-nest-servers` skill instead)5051## Framework Source Files (MUST READ before guessing)5253**ALWAYS read actual source code** from `node_modules/@lenne.tech/nuxt-extensions/` before guessing framework behavior. The framework ships documentation with the npm package.5455| File (in `node_modules/@lenne.tech/nuxt-extensions/`) | When to Read |56|-------------------------------------------------------|-------------|57| `CLAUDE.md` | Start of any frontend task — composables, components, config |58| `dist/runtime/composables/` | Available composables (`useLtAuth`, `useLtAuthClient`, `useLtTusUpload`, `useLtFile`, `useLtShare`, `useLtErrorTranslation`, and from 1.7.0 the `useLtAi*` family) |59| `dist/runtime/components/` | Available components |60| `dist/runtime/utils/` | Available utilities |61| `dist/runtime/types/` | TypeScript type definitions |6263**Also read** the nuxt-base-starter documentation:64- `README.md` — Project overview, tech stack, auth setup65- `AUTH.md` — Better Auth integration details6667## CRITICAL: Real Backend Integration FIRST6869**Never use placeholder data, TODO comments, or manual interfaces!**7071- Always use real API calls via `sdk.gen.ts` from the start72- Always use generated types from `types.gen.ts` (never manual interfaces for DTOs)73- Run `pnpm run generate-types` with API running before starting frontend work74- Implement feature-by-feature with full backend integration7576**Before starting:** Ensure services are running. See [reference/service-health-check.md](${CLAUDE_SKILL_DIR}/reference/service-health-check.md)7778## Skill Boundaries7980| User Intent | Correct Skill |81|------------|---------------|82| "Build a Vue component" | **THIS SKILL** |83| "Create a Nuxt page" | **THIS SKILL** |84| "Style with TailwindCSS" | **THIS SKILL** |85| "Create a NestJS module" | generating-nest-servers |86| "Security audit of frontend" | general-frontend-security |87| "Implement with TDD" | building-stories-with-tdd |8889## Related Skills9091**Works closely with:**92- `generating-nest-servers` - For NestJS backend development (projects/api/)93- `using-lt-cli` - For Git operations and Fullstack initialization94- `building-stories-with-tdd` - For complete TDD workflow (Backend + Frontend)95- `contributing-to-lt-framework` - When modifying `@lenne.tech/nuxt-extensions` itself and testing via `pnpm link`96- `/lt-dev:frontend:env-migrate` - Migrate env variables to `NUXT_` prefix convention9798## Dev Server Lifecycle99100When 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.101102**In monorepo projects:**103- `projects/app/` or `packages/app/` → **This skill**104- `projects/api/` or `packages/api/` → `generating-nest-servers` skill105106## Nuxt 4 Directory Structure107108```109app/ # Application code (srcDir)110├── components/ # Auto-imported components111├── composables/ # Auto-imported composables112├── interfaces/ # TypeScript interfaces113├── lib/ # Utility libraries (auth-client, etc.)114├── pages/ # File-based routing115├── layouts/ # Layout components116├── utils/ # Auto-imported utilities117└── api-client/ # Generated types & SDK118server/ # Nitro server routes119public/ # Static assets120nuxt.config.ts121```122123## Type Rules124125| Priority | Source | Use For |126|----------|--------|---------|127| 1. | `~/api-client/types.gen.ts` | All backend DTOs (REQUIRED) |128| 2. | `~/api-client/sdk.gen.ts` | All API calls (REQUIRED) |129| 3. | Nuxt UI types | Component props (auto-imported) |130| 4. | `app/interfaces/*.interface.ts` | Frontend-only types (UI state, forms) |131132## Standards133134| Rule | Value |135|------|-------|136| UI Labels | Match the project's language — detect, never assume (see Gotchas) |137| Code/Comments | English |138| Styling | TailwindCSS only, no `<style>` |139| Colors | Semantic only (`primary`, `error`, `success`) |140| Types | Explicit, no implicit `any` |141| Backend Types | **Generated only** (`types.gen.ts`) |142| Composables | `app/composables/use*.ts` |143| Shared State | `useState()` for SSR-safe state |144| Local State | `ref()` / `reactive()` |145| Forms | Valibot (not Zod) |146| Modals | `useOverlay()` |147148## Build Identity / Drift Detection149150The starter ships `/app/admin/system` + `useSystem()` to show which build runs151and detect a drifted / stale deployment (App vs. API on different commits):152153- App build is baked at build time into `runtimeConfig.public.appVersion` /154 `appCommit` (nuxt.config reads `package.json` version + `process.env.APP_VERSION_COMMIT`).155- API build is fetched from the public `GET /meta` via `buildLtApiUrl('/meta')`156 (auto-imported, SSR/proxy-aware) — never hardcode the API URL.157- Compare by **commit** only (`buildsMatch`); version numbers are per-component158 and may legitimately differ. `'unknown'` commits never trigger the warning.159- The Docker build must pass `APP_VERSION_COMMIT` (= CI commit SHA) *before*160 `nuxt build`, because `runtimeConfig.public` is frozen at build time.161162## TDD for Frontend163164```1651. Backend API must be complete (API tests pass)1662. Write E2E tests BEFORE implementing frontend1673. Implement components/pages until E2E tests pass1684. Debug with Chrome DevTools MCP169```170171**Complete E2E testing guide: [reference/e2e-testing.md](${CLAUDE_SKILL_DIR}/reference/e2e-testing.md)**172173## Error Handling — Consume Backend ErrorCodes via `useLtErrorTranslation`174175The 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.176177**NEVER assert or display raw English backend messages in the UI.** Always pipe errors through `translateError()` / `showErrorToast()` so users see localized text.178179```vue180<script setup lang="ts">181const { translateError, showErrorToast, parseError } = useLtErrorTranslation();182const toast = useToast();183184async function onSubmit() {185 try {186 await $fetch('/api/users', { method: 'POST', body: form.value });187 } catch (error) {188 // Preferred — direct toast from translated message189 showErrorToast(error, 'Speichern fehlgeschlagen');190191 // Or manual, if you need more control192 toast.add({193 color: 'error',194 title: 'Speichern fehlgeschlagen',195 description: translateError(error), // '#LTNS_0400: Resource not found' → 'Ressource nicht gefunden.'196 });197198 // Or parse for custom handling (e.g. redirect on specific code)199 const parsed = parseError(error);200 if (parsed.code === 'LTNS_0023') {201 await navigateTo('/auth/verify-email');202 }203 }204}205</script>206```207208**Rules:**209- [ ] Every error-handling site uses `useLtErrorTranslation()` — no raw `error.message` in Toast descriptions, form errors, or page-level error UI210- [ ] `loadTranslations(locale)` is called once at app start or on locale change (the composable caches per locale via `useState`)211- [ ] Code-based branching (`if (parsed.code === 'LTNS_XXXX')`) for flow-control decisions (verification-required redirects, retry prompts) — never branch on message-string contents212- [ ] 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`213- [ ] Tests assert translated messages (not English `error.message`) — see the test-reviewer rules in this plugin214215**Full consumer reference: [reference/error-translation.md](${CLAUDE_SKILL_DIR}/reference/error-translation.md)**216217## Reference Files218219| Topic | File |220|-------|------|221| Core Patterns | [reference/patterns.md](${CLAUDE_SKILL_DIR}/reference/patterns.md) |222| Service Health Check | [reference/service-health-check.md](${CLAUDE_SKILL_DIR}/reference/service-health-check.md) |223| Browser Testing | [reference/browser-testing.md](${CLAUDE_SKILL_DIR}/reference/browser-testing.md) |224| TypeScript | [reference/typescript.md](${CLAUDE_SKILL_DIR}/reference/typescript.md) |225| Components | [reference/components.md](${CLAUDE_SKILL_DIR}/reference/components.md) |226| Composables | [reference/composables.md](${CLAUDE_SKILL_DIR}/reference/composables.md) |227| Forms | [reference/forms.md](${CLAUDE_SKILL_DIR}/reference/forms.md) |228| Modals | [reference/modals.md](${CLAUDE_SKILL_DIR}/reference/modals.md) |229| API | [reference/api.md](${CLAUDE_SKILL_DIR}/reference/api.md) |230| Colors | [reference/colors.md](${CLAUDE_SKILL_DIR}/reference/colors.md) |231| Nuxt Patterns | [reference/nuxt.md](${CLAUDE_SKILL_DIR}/reference/nuxt.md) |232| Authentication | [reference/authentication.md](${CLAUDE_SKILL_DIR}/reference/authentication.md) |233| E2E Testing | [reference/e2e-testing.md](${CLAUDE_SKILL_DIR}/reference/e2e-testing.md) |234| Troubleshooting | [reference/troubleshooting.md](${CLAUDE_SKILL_DIR}/reference/troubleshooting.md) |235| Security | [reference/security.md](${CLAUDE_SKILL_DIR}/reference/security.md) |236| Error Translation (consume backend ErrorCodes) | [reference/error-translation.md](${CLAUDE_SKILL_DIR}/reference/error-translation.md) |237| Informed Trade-offs (Composition API, readonly, SSR guards, v-html, useFetch) | [reference/informed-trade-off-pattern.md](${CLAUDE_SKILL_DIR}/reference/informed-trade-off-pattern.md) |238239## Pre-Commit Checklist240241- [ ] No placeholder data, no TODO comments for API242- [ ] All API calls via `sdk.gen.ts`, all types from `types.gen.ts`243- [ ] Logic in composables, modals use `useOverlay`, forms use Valibot244- [ ] TailwindCSS only, semantic colors only245- [ ] UI text matches the project's detected language (not assumed German), code/comments English, no implicit `any`246- [ ] Auth uses `useLtAuth()`, protected routes use `middleware: 'auth'`247- [ ] 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)248- [ ] `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 them249- [ ] No `v-html` with user content, tokens stored securely250- [ ] All error-handling sites route through `useLtErrorTranslation()` — no raw backend messages in Toasts / UI251- [ ] Security review passed (`/lt-dev:review` for general scan)252- [ ] Feature tested in browser (Chrome DevTools MCP), no console errors
Run npx skillmds@latest add lennetech/developing-lt-frontend in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Handles ALL Nuxt 4 and Vue frontend work: components, composables, forms (Valibot), API integration (types.gen.ts, sdk.gen.ts), authentication (Better Auth), SSR, and Playwright E2E. Supports monorepos (projects/app/, packages/app/). Activates on .vue files, nuxt.config.ts, Nuxt UI, TailwindCSS, files under app/{components,composables,pages,interfaces,layouts}/, and on "generate types", "Valibot form", "useOverlay modal", "Playwright E2E", "Frontend bauen". NOT for NestJS backend (use generating-nest-servers). NOT for framework-agnostic security theory (use general-frontend-security). It is listed under Web & Frontend on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: makes network calls, reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
lenneTech (@lennetech) published this skill. Their other Agent Skills are listed on their SkillMD profile.