developing-lt-frontend

Installation
SKILL.md

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 instead of assuming 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:

Installs
14
First Seen
Feb 7, 2026