Guides

Troubleshooting

Common issues and solutions when working with Nuxt Crouton
Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data.

This guide covers common issues you might encounter when using Nuxt Crouton and how to resolve them.

Data Not Updating After Save

Problem

Table or list doesn't refresh after creating, updating, or deleting items.

Solution

Check that cache invalidation is working properly:

// In useCollectionMutation
const invalidateCache = async () => {
  await refreshNuxtData(`collection:${collection}:{}`)
}

Debug Steps

  1. Check the cache key format in your console (should show collection:shopProducts:{})
  2. Verify the mutation is calling invalidation:
const { create } = useCollectionMutation('shopProducts')

// Should automatically invalidate cache after success
await create({ name: 'New Product' })
  1. If using custom query parameters, ensure invalidation matches:
// All queries with shopProducts prefix should invalidate
await refreshNuxtData((key) => key.startsWith(`collection:shopProducts:`))

Common Causes

  • Collection name mismatch between query and mutation
  • Custom cache keys not being invalidated
  • API endpoint not returning updated data

Hot Reload Not Working

Problem

Changes to core library files in node_modules/@friendlyinternet/nuxt-crouton don't trigger hot reload.

Solution

Configure Vite to watch the core library:

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    server: {
      watch: {
        ignored: ['!**/node_modules/@friendlyinternet/**']
      }
    },
    optimizeDeps: {
      exclude: ['@friendlyinternet/nuxt-crouton']
    }
  }
})

Then Restart

# Kill dev server and restart
pnpm dev

Additional Steps

If the issue persists:

  1. Clear Nuxt cache:
rm -rf .nuxt
  1. Clear Vite cache:
rm -rf node_modules/.cache
  1. Restart dev server:
pnpm dev

Tailwind Classes Not Working

Problem

Tailwind CSS classes aren't being applied to Nuxt Crouton layer components:

  • Hover states don't work (e.g., hover:bg-primary)
  • Dynamic classes not applying
  • Only static classes from your app work, not from layer components
  • Classes work when used directly in your app, but not in layer components

Cause

This is expected behavior with Tailwind CSS v4 and Nuxt Layers. Tailwind's JIT compiler doesn't automatically scan components in external layers located in node_modules. This is not a bug—it's how Tailwind v4 is designed to work.

Solution

Add the @source directive to your app's main CSS file to explicitly tell Tailwind to scan the layer components.

1. Create or update your CSS file (e.g., app/assets/css/tailwind.css):

app/assets/css/tailwind.css
@import "tailwindcss";
@import "@nuxt/ui";

/* Scan Nuxt Crouton layers */
@source "../../../node_modules/@friendlyinternet/nuxt-crouton*/app/**/*.{vue,js,ts}";

2. Adjust the path based on your CSS file location:

  • If CSS is at app/assets/css/tailwind.css: use "../../../node_modules/..."
  • If CSS is at app.css: use "../node_modules/..."

3. Restart your dev server:

pnpm dev

Important Notes

  • Use relative paths only - Nuxt aliases like ~~, ~, or @ do NOT work in @source directives
  • Be specific - Don't use @source "../../node_modules/.c12/" as it's too broad and will cause timeouts
  • Use wildcards - The pattern nuxt-crouton*/ scans all Nuxt Crouton layers automatically

Verify It's Working

Test with a simple hover effect:

<template>
  <div class="p-4 bg-gray-100 hover:bg-primary transition-colors">
    Hover over me
  </div>
</template>

If the hover effect works after adding @source, the configuration is correct.

Alternative: List Layers Explicitly

Instead of the wildcard pattern, you can list each layer:

@source "../../../node_modules/@friendlyinternet/nuxt-crouton/app/**/*.{vue,js,ts}";
@source "../../../node_modules/@friendlyinternet/nuxt-crouton-i18n/app/**/*.{vue,js,ts}";
@source "../../../node_modules/@friendlyinternet/nuxt-crouton-editor/app/**/*.{vue,js,ts}";
@source "../../../node_modules/@friendlyinternet/nuxt-crouton-assets/app/**/*.{vue,js,ts}";
@source "../../../node_modules/@friendlyinternet/nuxt-crouton-supersaas/app/**/*.{vue,js,ts}";

This is a known limitation of Tailwind CSS v4 with Nuxt Layers:

See Installation Guide - Configure Tailwind CSS for complete setup instructions.


Form Not Opening

Problem

Clicking a button to open a form does nothing. No modal or slideover appears.

Debug Steps

  1. Check if useCrouton state is updating:
<script setup>
const { open, showCrouton } = useCrouton()

const handleClick = () => {
  console.log('Before:', showCrouton.value)
  open('create', 'shopProducts')
  console.log('After:', showCrouton.value)
}
</script>
  1. Verify collection is registered in app.config.ts:
// app.config.ts
export default defineAppConfig({
  croutonCollections: {
    shopProducts: {
      name: 'shopProducts',
      layer: 'shop',
      componentName: 'ShopProductsForm',
      apiPath: 'shop-products',
    }
  }
})
  1. Check component naming matches:
layers/shop/components/products/Form.vue
↓
Component name: ShopProductsForm (PascalCase)

Common Causes

  • Collection not registered in app.config.ts
  • Typo in collection name (case-sensitive)
  • Form component doesn't exist or has wrong name
  • Modal container component not imported

Solution

Verify the full chain:

  1. Collection registered: ✅
  2. Component exists: ✅ layers/shop/components/products/Form.vue
  3. Component name matches: ✅ ShopProductsForm
  4. Container component imported: ✅ Check app layout

Type Errors After Generation

Problem

TypeScript shows errors like "Cannot find module" or "Property does not exist" after generating a collection.

Solution 1: Restart TypeScript Server

In VS Code:

  1. Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux)
  2. Type "Restart TS Server"
  3. Select "TypeScript: Restart TS Server"

Solution 2: Clear Nuxt Cache

rm -rf .nuxt
npx nuxt prepare

Solution 3: Regenerate Types

npx nuxt typecheck

Common Type Errors

Missing Module Error

Cannot find module '~/layers/shop/types/products'

Fix: Ensure the file exists and path is correct

ls layers/shop/types/products.ts

Property Does Not Exist

Property 'name' does not exist on type 'never'

Fix: Add type parameter to query

// ❌ Bad
const { items } = await useCollectionQuery('shopProducts')

// ✅ Good
import type { ShopProduct } from '~/layers/shop/types/products'
const { items } = await useCollectionQuery<ShopProduct>('shopProducts')

Full Reset Procedure

If all else fails:

# 1. Clear all caches
rm -rf .nuxt node_modules/.cache

# 2. Reinstall dependencies
pnpm install

# 3. Regenerate types
npx nuxt prepare

# 4. Restart dev server
pnpm dev

Duplicate Key Errors During Build

Problem

Getting duplicate key errors when building or running the development server:

[esbuild] (schema.ts:8:2) Duplicate key "userId" in object literal
[esbuild] (schema.ts:9:2) Duplicate key "teamId" in object literal
[esbuild] (schema.ts:10:2) Duplicate key "createdAt" in object literal
[esbuild] (schema.ts:11:2) Duplicate key "updatedAt" in object literal

Cause

You've manually defined auto-generated fields (teamId, userId, createdAt, or updatedAt) in your schema JSON files. The generator adds these fields automatically based on your configuration flags, so manual definitions create duplicates.

Solution 1: Remove Fields from Schema

Remove these fields from your schema JSON files:

{
  "userId": {           // ❌ Remove this
    "type": "string",
    "refTarget": "users"
  },
  "teamId": {           // ❌ Remove this
    "type": "string"
  },
  "createdAt": {        // ❌ Remove this
    "type": "date"
  },
  "updatedAt": {        // ❌ Remove this
    "type": "date"
  },
  "title": {            // ✅ Keep your custom fields
    "type": "string"
  }
}

Solution 2: Adjust Configuration Flags

If you don't need certain auto-generated fields, disable them in your config:

// crouton.config.js
export default {
  flags: {
    useTeamUtility: false,  // Disables teamId & userId
    useMetadata: false      // Disables createdAt & updatedAt
  }
}

Understanding Auto-Generated Fields

The generator automatically adds fields based on configuration:

Always added:

  • id - Primary key

When useTeamUtility: true:

  • teamId - Team/organization reference
  • userId - User who created the record

When useMetadata: true (default):

  • createdAt - Creation timestamp
  • updatedAt - Last update timestamp

After Fixing

  1. Update your schema JSON files to remove auto-generated fields
  2. Regenerate the collection:
npx crouton-generate config ./crouton.config.js --force
  1. Restart your dev server:
pnpm dev

See Schema Format - Auto-Generated Fields for complete details.


Delete Button Error

Problem

Error: send is not a function when clicking delete button.

Cause

Using old generated code with the new core library. The send() method was removed in v2.0.

Solution 1: Regenerate Form

npx crouton-generate shop products --fields-file schema.json --force

Solution 2: Manual Update

Update your Form.vue manually:

Before (v1.x):

<script setup>
const { send } = useCrouton()

const handleSubmit = async () => {
  await send('create', 'shopProducts', state.value)
}
</script>

After (v2.0):

<script setup>
const { create, update, deleteItems } = useCollectionMutation('shopProducts')

const handleSubmit = async () => {
  if (props.action === 'create') {
    await create(state.value)
  } else if (props.action === 'update') {
    await update(state.value.id, state.value)
  } else if (props.action === 'delete') {
    await deleteItems(props.items)
  }
  close()
}
</script>

See Migration Guide for complete migration instructions.


Cache Not Invalidating

Problem

Multiple views of the same data, but only one updates after mutation.

Debug

Check if you're using different cache keys:

// List view
const { items } = await useCollectionQuery('shopProducts', {
  query: computed(() => ({ page: 1 }))  // Cache key: collection:shopProducts:{"page":1}
})

// Detail view
const { items } = await useCollectionQuery('shopProducts', {
  query: computed(() => ({ page: 2 }))  // Cache key: collection:shopProducts:{"page":2}
})

Solution

useCollectionMutation already invalidates all matching queries:

// This invalidates ALL shopProducts queries, regardless of parameters
await refreshNuxtData((key) => key.startsWith(`collection:shopProducts:`))

If you have custom invalidation, ensure it matches all variants:

// ❌ Bad - only invalidates exact match
await refreshNuxtData(`collection:shopProducts:{}`)

// ✅ Good - invalidates all variants
await refreshNuxtData((key) => key.startsWith(`collection:shopProducts:`))

Validation Errors Not Showing

Problem

Form submits even with invalid data, or validation errors don't display.

Check Schema Setup

// composables/useProducts.ts
import { z } from 'zod'

export function useShopProducts() {
  const schema = z.object({
    name: z.string().min(1, 'Name is required'),
    price: z.number().min(0, 'Price must be positive')
  })

  return { schema }
}

Check Form Usage

<template>
  <UForm
    :state="state"
    :schema="schema"
    @submit="handleSubmit"
  >
    <UFormField label="Name" name="name">
      <UInput v-model="state.name" />
    </UFormField>
  </UForm>
</template>

<script setup lang="ts">
const { schema } = useShopProducts()
const state = ref({ name: '', price: 0 })
</script>

Common Issues

  1. Schema not passed to form:
    • Ensure :schema="schema" prop is set
  2. Field name mismatch:
    • name="name" must match schema key
    • Check spelling and case
  3. State not reactive:
    • Use ref() or reactive() for form state

Layer Pages 404 Errors

Problem

Getting 404 errors when navigating to Nuxt Crouton pages like /dashboard/[team]/crouton:

404 - Page not found: /dashboard/myteam/crouton

Cause

You're using Nuxt 3 instead of Nuxt 4. Nuxt Crouton uses the app/pages/ directory structure (Nuxt 4 convention). When the consuming app uses Nuxt 3, Nuxt looks for pages in pages/ instead of app/pages/, so layer pages are not found.

Solution

Upgrade your project to Nuxt 4:

package.json
{
  "dependencies": {
    "nuxt": "^4.0.0",
    "@nuxt/ui": "^4.0.0"
  }
}

Then reinstall dependencies:

pnpm install

Verification

After upgrading, verify your project structure follows Nuxt 4 conventions:

app/
  ├── pages/          # Nuxt 4 location
  ├── components/
  ├── composables/
  └── app.vue
Nuxt 4 is required. Nuxt Crouton will not work correctly with Nuxt 3 due to directory structure differences. See Installation Requirements for details.

API Route 404 Errors

Problem

Getting 404 errors when trying to fetch or mutate data.

Check API Route Paths

Ensure your API routes match the collection's apiPath:

// app.config.ts
export default defineAppConfig({
  croutonCollections: {
    shopProducts: {
      apiPath: 'shop-products',  // ← This determines the route
    }
  }
})

Should match:

server/api/teams/[team]/shop-products/
  ├── index.get.ts        # GET /api/teams/:team/shop-products
  ├── index.post.ts       # POST /api/teams/:team/shop-products
  ├── [id].patch.ts       # PATCH /api/teams/:team/shop-products/:id
  └── [id].delete.ts      # DELETE /api/teams/:team/shop-products/:id

Check Team-Based Routes

If using team-based auth:

// server/api/teams/[team]/shop-products/index.get.ts
export default defineEventHandler(async (event) => {
  const teamId = getRouterParam(event, 'team')

  if (!teamId) {
    throw createError({ statusCode: 400, message: 'Team ID required' })
  }

  // Your query logic
})

Debug API Calls

Check browser DevTools Network tab:

  1. Look for failed requests
  2. Check request URL matches expected pattern
  3. Verify request method (GET, POST, PATCH, DELETE)
  4. Check response error messages

Translation Issues

Problem

Translations not loading or switching locales doesn't update content.

Check i18n Setup

Ensure @friendlyinternet/nuxt-crouton-i18n is included in your nuxt.config.ts extends array.

Check Query Locale Binding

Make sure your query reactively binds to the i18n locale using a computed query parameter (see Querying with Filters for the pattern).

Check Translation Fields

Ensure fields are marked as translatable in config:

// crouton.config.js
export default {
  translations: {
    collections: {
      products: ['name', 'description']  // ← Mark translatable fields
    }
  }
}

Customizing Auto-Generated Repeater Components

Overview

When you define a repeater field in your schema, the generator automatically creates a placeholder component for you. This placeholder is fully functional but needs customization to match your specific data structure.

What You Get

The auto-generated placeholder component includes:

  • Proper Vue component structure (props, emits, v-model)
  • TypeScript interface with TODO comments
  • Default values in computed getter
  • ID field (auto-generated, disabled)
  • Debug section showing raw data
  • Visual indicators (dashed border, gray background)

Example placeholder location:

layers/bookings/collections/locations/app/components/Slot.vue

How to Customize

Step 1: Update the Interface

Define the fields your repeater item needs:

// Before (placeholder)
interface SlotItem {
  id: string
  // TODO: Add your fields here
}

// After (customized)
interface SlotItem {
  id: string
  label: string
  startTime: string
  endTime: string
}

Step 2: Add Default Values

Provide sensible defaults for your fields:

import { nanoid } from 'nanoid'

// Before
const localValue = computed({
  get: () => props.modelValue || {
    id: nanoid(),
    // TODO: Add default values for your fields
  },
  set: (val) => emit('update:modelValue', val)
})

// After
const localValue = computed({
  get: () => props.modelValue || {
    id: nanoid(),
    label: '',
    startTime: '09:00',
    endTime: '17:00'
  },
  set: (val) => emit('update:modelValue', val)
})

Step 3: Update the Template

Replace the placeholder with your custom form fields:

<template>
  <!-- Remove the TODO section and placeholder styling -->
  <div class="grid grid-cols-4 gap-4">
    <UFormField label="ID" name="id">
      <UInput v-model="localValue.id" disabled />
    </UFormField>

    <UFormField label="Label" name="label">
      <UInput v-model="localValue.label" />
    </UFormField>

    <UFormField label="Start Time" name="startTime">
      <UInput v-model="localValue.startTime" type="time" />
    </UFormField>

    <UFormField label="End Time" name="endTime">
      <UInput v-model="localValue.endTime" type="time" />
    </UFormField>
  </div>
</template>

Complete Customized Example

Before (auto-generated):

<script setup lang="ts">
import { nanoid } from 'nanoid'

interface SlotItem {
  id: string
  // TODO: Add your fields here
}

const props = defineProps<{ modelValue: SlotItem }>()
const emit = defineEmits<{ 'update:modelValue': [value: SlotItem] }>()

const localValue = computed({
  get: () => props.modelValue || { id: nanoid() },
  set: (val) => emit('update:modelValue', val)
})
</script>

<template>
  <div class="space-y-3 p-4 bg-gray-50 rounded-lg border border-dashed">
    <div class="text-sm text-gray-600">
      <strong>TODO:</strong> Customize this component
    </div>
    <!-- Placeholder fields -->
  </div>
</template>

After (customized):

<script setup lang="ts">
import { nanoid } from 'nanoid'

interface SlotItem {
  id: string
  label: string
  startTime: string
  endTime: string
}

const props = defineProps<{ modelValue: SlotItem }>()
const emit = defineEmits<{ 'update:modelValue': [value: SlotItem] }>()

const localValue = computed({
  get: () => props.modelValue || {
    id: nanoid(),
    label: '',
    startTime: '09:00',
    endTime: '17:00'
  },
  set: (val) => emit('update:modelValue', val)
})
</script>

<template>
  <div class="grid grid-cols-4 gap-4">
    <UFormField label="ID" name="id">
      <UInput v-model="localValue.id" disabled />
    </UFormField>

    <UFormField label="Label" name="label">
      <UInput v-model="localValue.label" />
    </UFormField>

    <UFormField label="Start Time" name="startTime">
      <UInput v-model="localValue.startTime" type="time" />
    </UFormField>

    <UFormField label="End Time" name="endTime">
      <UInput v-model="localValue.endTime" type="time" />
    </UFormField>
  </div>
</template>

Tips

  • The placeholder works immediately—you can test your repeater before customizing
  • Keep the ID field disabled to prevent accidental modification
  • The debug section in the placeholder helps verify data structure during development
  • Remove placeholder styling (dashed borders, gray background) when ready for production

See Schema Format - Repeater Fields for more details on defining repeater fields.


Performance Issues

Problem

Slow page loads or laggy UI when working with large datasets.

Solutions

1. Implement Pagination

Use pagination with page and limit query parameters (see Querying with Pagination).

2. Use Server-Side Filtering

// server/api/teams/[team]/products/index.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const { page = 1, limit = 50, search } = query

  // Filter in database, not in frontend
  let dbQuery = db.select().from(products)

  if (search) {
    dbQuery = dbQuery.where(like(products.name, `%${search}%`))
  }

  return dbQuery
    .limit(limit)
    .offset((page - 1) * limit)
})

3. Optimize Relations

Use server-side joins instead of multiple queries to avoid N+1 query problems. See Querying with Relations for the recommended pattern.


Getting More Help

If you're still experiencing issues:

  1. Check GitHub Issues: Search for similar problems at github.com/pmcp/nuxt-crouton/issues
  2. Create an Issue: Include:
    • Nuxt Crouton version
    • Nuxt version
    • Node version
    • Error messages
    • Minimal reproduction steps
  3. Ask in Discussions: For general questions, use GitHub Discussions