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.
Table or list doesn't refresh after creating, updating, or deleting items.
Check that cache invalidation is working properly:
// In useCollectionMutation
const invalidateCache = async () => {
await refreshNuxtData(`collection:${collection}:{}`)
}
collection:shopProducts:{})const { create } = useCollectionMutation('shopProducts')
// Should automatically invalidate cache after success
await create({ name: 'New Product' })
// All queries with shopProducts prefix should invalidate
await refreshNuxtData((key) => key.startsWith(`collection:shopProducts:`))
Changes to core library files in node_modules/@fyit/crouton don't trigger hot reload.
Configure Vite to watch the core library:
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
server: {
watch: {
ignored: ['!**/node_modules/@fyit/**']
}
},
optimizeDeps: {
exclude: ['@fyit/crouton']
}
}
})
# Kill dev server and restart
pnpm dev
If the issue persists:
rm -rf .nuxt
rm -rf node_modules/.cache
pnpm dev
Tailwind CSS classes aren't being applied to Nuxt Crouton layer components:
hover:bg-primary)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.
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):
@import "tailwindcss";
@import "@nuxt/ui";
/* Scan Nuxt Crouton layers */
@source "../../../node_modules/@fyit/crouton*/app/**/*.{vue,js,ts}";
2. Adjust the path based on your CSS file location:
app/assets/css/tailwind.css: use "../../../node_modules/..."app.css: use "../node_modules/..."3. Restart your dev server:
pnpm dev
~~, ~, or @ do NOT work in @source directives@source "../../node_modules/.c12/" as it's too broad and will cause timeoutsnuxt-crouton*/ scans all Nuxt Crouton layers automaticallyTest 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.
Instead of the wildcard pattern, you can list each layer:
@source "../../../node_modules/@fyit/crouton/app/**/*.{vue,js,ts}";
@source "../../../node_modules/@fyit/crouton-i18n/app/**/*.{vue,js,ts}";
@source "../../../node_modules/@fyit/crouton-editor/app/**/*.{vue,js,ts}";
@source "../../../node_modules/@fyit/crouton-assets/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.
Clicking a button to open a form does nothing. No modal or slideover appears.
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>
app.config.ts:// app.config.ts
export default defineAppConfig({
croutonCollections: {
shopProducts: {
name: 'shopProducts',
layer: 'shop',
componentName: 'ShopProductsForm',
apiPath: 'shop-products',
}
}
})
layers/shop/components/products/Form.vue
↓
Component name: ShopProductsForm (PascalCase)
app.config.tsVerify the full chain:
layers/shop/components/products/Form.vueShopProductsFormTypeScript shows errors like "Cannot find module" or "Property does not exist" after generating a collection.
In VS Code:
Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux)rm -rf .nuxt
npx nuxt prepare
npx nuxt typecheck
Cannot find module '~/layers/shop/types/products'
Fix: Ensure the file exists and path is correct
ls layers/shop/types/products.ts
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')
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
Getting duplicate key errors when building or running the development server:
[esbuild] (schema.ts:8:2) Duplicate key "owner" 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
You've manually defined auto-generated fields (teamId, owner, createdAt, updatedAt, createdBy, or updatedBy) in your schema JSON files. The generator adds these fields automatically based on your configuration flags, so manual definitions create duplicates.
Remove these fields from your schema JSON files:
{
"owner": { // ❌ 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"
}
}
If you don't need certain auto-generated fields, disable them in your config:
// crouton.config.js
export default {
flags: {
useMetadata: false // Disables createdAt & updatedAt
}
}
The generator automatically adds fields:
Always added:
id - Primary keyteamId - Team/organization reference (for team-scoped collections)owner - User who owns the record (for team-scoped collections)When useMetadata: true (default):
createdAt - Creation timestampupdatedAt - Last update timestampcreatedBy - User who created the recordupdatedBy - User who last updated the recordnpx crouton-generate config ./crouton.config.js --force
pnpm dev
See Schema Format - Auto-Generated Fields for complete details.
Error: send is not a function when clicking delete button.
Using old generated code with the new core library. The send() method was removed in v2.0.
npx crouton-generate shop products --fields-file schema.json --force
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.
Multiple views of the same data, but only one updates after mutation.
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}
})
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:`))
Form submits even with invalid data, or validation errors don't display.
// 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 }
}
<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>
:schema="schema" prop is setname="name" must match schema keyref() or reactive() for form stateGetting 404 errors when navigating to Nuxt Crouton pages like /admin/[team]/collections or /super-admin:
404 - Page not found: /admin/myteam/collections
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.
Upgrade your project to Nuxt 4:
{
"dependencies": {
"nuxt": "^4.0.0",
"@nuxt/ui": "^4.3.0"
}
}
Then reinstall dependencies:
pnpm install
After upgrading, verify your project structure follows Nuxt 4 conventions:
app/
├── pages/ # Nuxt 4 location
├── components/
├── composables/
└── app.vue
Nuxt Crouton uses a three-tier route architecture:
| Tier | Route Pattern | Purpose | Access |
|---|---|---|---|
| User | /dashboard/[team]/* | User-facing features | Any team member |
| Admin | /admin/[team]/* | Team management, collections | Team admins/owners |
| Super Admin | /super-admin/* | System-wide management | App owner only |
/auth/login - Login page/auth/register - Registration page/dashboard - Team selection/dashboard/[team] - User dashboard/admin/[team]/collections - Collection management/admin/[team]/members - Team members/admin/[team]/settings - Team settings/super-admin - Super admin dashboard/super-admin/users - User management/super-admin/teams - Team managementGetting 404 errors when trying to fetch or mutate data.
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
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({ status: 400, statusText: 'Team ID required' })
}
// Your query logic
})
Check browser DevTools Network tab:
Translations not loading or switching locales doesn't update content.
Ensure @fyit/crouton-i18n is included in your nuxt.config.ts extends array.
Make sure your query reactively binds to the i18n locale using a computed query parameter (see Querying with Filters for the pattern).
Ensure fields are marked as translatable in config:
// crouton.config.js
export default {
translations: {
collections: {
products: ['name', 'description'] // ← Mark translatable fields
}
}
}
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.
The auto-generated placeholder component includes:
Example placeholder location:
layers/bookings/collections/locations/app/components/Slot.vue
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
}
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)
})
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>
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>
See Schema Format - Repeater Fields for more details on defining repeater fields.
Slow page loads or laggy UI when working with large datasets.
Use pagination with page and limit query parameters (see Querying with Pagination).
// 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)
})
Use server-side joins instead of multiple queries to avoid N+1 query problems. See Querying with Relations for the recommended pattern.
If you're still experiencing issues: