Skip to content
developing-lt-frontend logo

lenne.tech Frontend Development

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...

SKILL.md

Full skill instructions

lenne.tech Frontend Development

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)

Development Approach: Real Backend Integration FIRST

CRITICAL: Always implement with real backend integration immediately!

┌────────────────────────────────────────────────────────────────┐
│  ❌ FORBIDDEN:                                                 │
│  - Placeholder data: const items = ['Item 1', 'Item 2']        │
│  - TODO comments: // TODO: Connect to API later                │
│  - Mock functions: async function fetchData() { return [] }    │
│  - Dummy interfaces: interface Item { name: string }           │
│  - "We'll add the API call later"                              │
│                                                                │
│  ✅ REQUIRED:                                                  │
│  - Real API calls from the start                               │
│  - Generated types (types.gen.ts)                              │
│  - Generated SDK functions (sdk.gen.ts)                        │
│  - Feature-by-feature with full backend integration            │
└────────────────────────────────────────────────────────────────┘

Workflow for each feature:

  1. Ensure services are running (API on 3000, App on 3001)
  2. Generate types first (npm run generate-types)
  3. Create composable with real SDK functions
  4. Build component using the composable
  5. Test in browser with real data
  6. Move to next feature only when current one works

Example - The RIGHT way:

// ✅ CORRECT: Real integration from the start
import type { ProductDto } from '~/​api-client/​types.gen'
import { productControllerFindAll } from '~/​api-client/​sdk.gen'

export function useProducts() {
  const products = ref<ProductDto[]>([])

  async function fetchAll() {
    const response = await productControllerFindAll()
    if (response.data) products.value = response.data
  }

  return { products: readonly(products), fetchAll }
}
// ❌ WRONG: Placeholder approach
interface Product { name: string }  // Manual interface!
const products = ref<Product[]>([
  { name: 'Placeholder 1' },  // Fake data!
  { name: 'Placeholder 2' }
])
// TODO: Connect to API  // Deferred work!

Why this matters:

  • Placeholders hide integration issues until later
  • Manual interfaces drift from backend DTOs
  • "Later" often means bugs discovered too late
  • Real data reveals edge cases immediately

Test-Driven Development (TDD)

For frontend features, follow the TDD approach:

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

Quick E2E Test Example

// tests/​e2e/​products.spec.ts
import { test, expect } from '@playwright/​test';

test('should create product', async ({ page }) => {
  await page.goto('/​products');
  await page.click('[data-testid="create"]');
  await page.fill('[data-testid="name"]', `Test-${Date.now()}`);
  await page.click('[data-testid="submit"]');
  await expect(page.locator('text=erfolgreich')).toBeVisible();
});

Test Cleanup (CRITICAL)

Every E2E test must clean up after itself:

test.afterAll(async ({ request }) => {
  for (const id of createdIds) {
    await request.delete(`/​api/​products/​${id}`);
  }
});

Use separate test database: app-test instead of app-dev

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)

When to use which:

  • .vue files, Nuxt, Vue components? Use this skill (developing-lt-frontend)
  • NestJS, services, controllers? Use generating-nest-servers skill
  • Git operations, lt commands? Use using-lt-cli skill
  • Complete TDD workflow (tests first)? Use building-stories-with-tdd skill

In monorepo projects:

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

TypeScript Language Server (Recommended)

Use the LSP tool when available for better code intelligence in TypeScript/​Vue/​Nuxt projects:

OperationUse Case
goToDefinitionFind where a type, composable, or component is defined
findReferencesFind all usages of a symbol across the codebase
hoverGet type information for props, refs, and computed values
documentSymbolList all exports, functions, and types in a file
workspaceSymbolSearch for composables, interfaces, or components
goToImplementationFind implementations of interfaces

When to use LSP:

  • Finding where a type from types.gen.ts is used → findReferences
  • Understanding composable structure → documentSymbol
  • Navigating to type definitions → goToDefinition
  • Searching for components or composables → workspaceSymbol

Installation (if LSP not available):

claude plugins install typescript-lsp --marketplace claude-plugins-official

Service Health Check (MANDATORY)

Before starting ANY frontend work, check if required services are running:

# Check if API is running (Port 3000)
curl -s -o /​dev/​null -w "%{http_code}" http://localhost:3000/api

# Check if App is running (Port 3001)
curl -s -o /​dev/​null -w "%{http_code}" http://localhost:3001

Workflow:

┌────────────────────────────────────────────────────────────────┐
│  BEFORE starting frontend work:                                │
│                                                                │
│  1. CHECK API (Port 3000):                                     │
│     curl -s -o /​dev/​null -w "%{http_code}" localhost:3000/​api  │
│     - If NOT 200: Start API in background                      │
│       cd projects/​api && npm run start:dev &                   │
│     - Wait until API responds (max 30s)                        │
│                                                                │
│  2. CHECK APP (Port 3001):                                     │
│     curl -s -o /​dev/​null -w "%{http_code}" localhost:3001      │
│     - If NOT 200: Start App in background                      │
│       cd projects/​app && npm run dev &                         │
│     - Wait until App responds (max 30s)                        │
│                                                                │
│  3. ONLY THEN proceed with frontend development                │
└────────────────────────────────────────────────────────────────┘

Starting Services (if not running):

# Start API in background (from monorepo root)
cd projects/​api && npm run start:dev &

# Start App in background (from monorepo root)
cd projects/​app && npm run dev &

Important:

  • Always check BEFORE starting to avoid duplicate processes
  • Use lsof -i :3000 or lsof -i :3001 to check if port is already in use
  • If port is in use but service not responding, investigate before starting another instance

Browser Testing (Chrome DevTools MCP)

After implementing each feature, verify it works in the browser!

Available Tools:

ToolUse Case
mcp__chrome-devtools__navigate_pageNavigate to URL
mcp__chrome-devtools__take_snapshotGet page structure with UIDs (preferred)
mcp__chrome-devtools__take_screenshotVisual verification
mcp__chrome-devtools__click / fillInteract with elements
mcp__chrome-devtools__list_console_messagesCheck for JS errors
mcp__chrome-devtools__list_network_requestsDebug API calls

Workflow after each feature:

┌────────────────────────────────────────────────────────────────┐
│  AFTER implementing a feature:                                 │
│                                                                │
│  1. NAVIGATE to the page:                                      │
│     mcp__chrome-devtools__navigate_page(url: "localhost:3001") │
│                                                                │
│  2. TAKE SNAPSHOT (preferred over screenshot):                 │
│     mcp__chrome-devtools__take_snapshot()                      │
│     - Check if on correct page (middleware may redirect)       │
│     - If redirected to /​login: Handle authentication first     │
│                                                                │
│  3. CHECK CONSOLE for errors:                                  │
│     mcp__chrome-devtools__list_console_messages(types: error)  │
│     - Fix any JavaScript errors before proceeding              │
│                                                                │
│  4. VERIFY API calls work:                                     │
│     mcp__chrome-devtools__list_network_requests()              │
│     - Check for failed requests (4xx, 5xx)                     │
│                                                                │
│  5. ONLY proceed to next feature when current one works        │
└────────────────────────────────────────────────────────────────┘

Authentication Handling:

  • Most pages require login
  • If redirected to /​login or /​auth/​login: Ask user for credentials
  • Use fill and click tools to authenticate
  • Then navigate back to intended page

Nuxt UI MCP (Component Documentation)

Use the Nuxt UI MCP tools for component documentation:

ToolUse Case
mcp__nuxt-ui-remote__list-componentsList all available components
mcp__nuxt-ui-remote__get-componentGet component documentation
mcp__nuxt-ui-remote__get-component-metadataGet props, slots, events
mcp__nuxt-ui-remote__search-components-by-categoryFind components by category
mcp__nuxt-ui-remote__list-composablesList available composables

When to use:

  • Before using a Nuxt UI component you haven't used before
  • When unsure about available props or slots
  • When looking for the right component for a use case

Error Recovery

For detailed troubleshooting workflows, see reference/​troubleshooting.md

Quick fixes:

  • Type generation fails → Check if API is running on port 3000
  • API won't start → Check lsof -i :3000, kill stale processes
  • Build fails → Run npm run generate-types, check imports
  • Console errors → Use mcp__chrome-devtools__list_console_messages

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

CRITICAL: Never create custom interfaces for backend DTOs!

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)

Missing Generated Types

If types.gen.ts or sdk.gen.ts are missing/​outdated: See reference/​troubleshooting.md

Key rules:

  • ❌ NEVER create manual interfaces as workaround
  • ✅ Always run npm run generate-types with API running

Generating Types

Prerequisites: Backend API must be running!

# Start API first (in monorepo)
cd projects/​api && npm run start:dev

# Wait for API to be ready (check http://localhost:3000/api)

# Then generate types (from frontend directory)
npm run generate-types

Core Patterns

API Calls (via generated SDK)

import type { SeasonDto } from '~/​api-client/​types.gen'
import { seasonControllerGet } from '~/​api-client/​sdk.gen'

const response = await seasonControllerGet()
const seasons: SeasonDto[] = response.data ?? []

Composables (one per controller)

export function useSeasons() {
  const seasons = ref<SeasonDto[]>([])
  const loading = ref<boolean>(false)

  async function fetchSeasons(): Promise<void> {
    loading.value = true
    try {
      const response = await seasonControllerGet()
      if (response.data) seasons.value = response.data
    } finally {
      loading.value = false
    }
  }

  return { seasons: readonly(seasons), loading: readonly(loading), fetchSeasons }
}

Shared State (useState)

// For state shared across components (SSR-safe)
export function useSettings() {
  const theme = useState<'light' | 'dark'>('app-theme', () => 'light')
  return { theme }
}

Authentication (Better Auth)

// app/​composables/​use-better-auth.ts (pre-configured in nuxt-base-starter)
import { authClient } from '~/​lib/​auth-client'

export function useBetterAuth() {
  const session = authClient.useSession(useFetch)

  const user = computed(() => session.data.value?.user ?? null)
  const isAuthenticated = computed<boolean>(() => !!session.data.value?.session)
  const isAdmin = computed<boolean>(() => user.value?.role === 'admin')

  return {
    user, isAuthenticated, isAdmin,
    signIn: authClient.signIn,   // Password auto-hashed (SHA256)
    signUp: authClient.signUp,   // Password auto-hashed (SHA256)
    signOut: authClient.signOut,
    twoFactor: authClient.twoFactor,
    passkey: authClient.passkey,
  }
}

Preferred auth methods: Passkey (WebAuthn) or Email/​Password + 2FA (TOTP) Base path: /​iam (must match nest-server config)

Programmatic Modals

const overlay = useOverlay()

overlay.open(ModalCreate, {
  props: { title: 'Neu' },
  onClose: (result) => { if (result) refreshData() }
})

Valibot Forms (not Zod)

import { object, pipe, string, minLength } from 'valibot'
import type { InferOutput } from 'valibot'

const schema = object({
  title: pipe(string(), minLength(3, 'Mindestens 3 Zeichen'))
})
type Schema = InferOutput<typeof schema>
const state = reactive<Schema>({ title: '' })

Standards

RuleValue
UI LabelsGerman (Speichern, Abbrechen)
Code/​CommentsEnglish
StylingTailwindCSS only, no <style>
ColorsSemantic only (primary, error, success), no hardcoded
TypesExplicit, no implicit any
Backend TypesGenerated only (types.gen.ts)
Custom InterfacesFrontend-only (app/​interfaces/​*.interface.ts)
Composablesapp/​composables/​use*.ts
Shared StateuseState() for SSR-safe state
Local Stateref() / reactive()

Reference Files

Pre-Commit

Backend Integration (CRITICAL):

  • No placeholder data (no fake arrays, no dummy objects)
  • No TODO comments for API integration (integrate immediately)
  • No manual interfaces for backend DTOs (use types.gen.ts)
  • All API calls via sdk.gen.ts
  • Types regenerated after backend changes (npm run generate-types)
  • Each feature fully integrated before starting next

Code Quality:

  • Logic in composables
  • Modals use useOverlay
  • Forms use Valibot
  • TailwindCSS only
  • Semantic colors only (primary, error, success), no hardcoded colors
  • German UI, English code
  • No implicit any
  • ESLint passes

Authentication:

  • Auth uses useBetterAuth() composable (pre-configured)
  • Protected routes use middleware: 'auth'
  • Auth base path is /​iam (nest-server default)

Security (OWASP):

  • No v-html with user content (see security.md)
  • Tokens stored securely (not in localStorage)
  • Input validation with Valibot schemas

Browser Verification:

  • Feature tested in browser (Chrome DevTools MCP)
  • No console errors (list_console_messages)
  • API calls successful (list_network_requests)
  • Page renders correctly (snapshot or screenshot)