This guide documents the conventions used throughout Nuxt Crouton. Following these conventions ensures consistency, predictability, and easier collaboration.
Collections should follow these rules:
products, users, ordersblogPosts, orderItems, userProfilesuserProfiles is better than just profilescategories not catsitems, data, recordsExamples:
✅ Good: products, blogPosts, orderItems
❌ Bad: product (singular), blog_posts (snake_case), items (too generic), prods (abbreviated)
Field names should:
firstName, createdAt, isActiveemailAddress not email if you also have emailVerifiedcreatedAt, updatedAt, publishedAt, deletedAtisActive, hasAccess, canEdituserId, categoryId, authorIdExamples:
{
"firstName": { "type": "string" }, // ✅ Good
"isPublished": { "type": "boolean" }, // ✅ Good
"publishedAt": { "type": "date" }, // ✅ Good
"authorId": { "type": "string", "refTarget": "authors" }, // ✅ Good
"first_name": { "type": "string" }, // ❌ Bad: snake_case
"published": { "type": "boolean" }, // ❌ Bad: not clear it's a boolean
"pub_date": { "type": "date" }, // ❌ Bad: abbreviated
"author": { "type": "string" } // ❌ Bad: should be authorId
}
Generated and custom files follow these patterns:
Component Files:
_Form.vue, List.vueProductCard.vue, OrderStatusBadge.vue, UserAvatar.vueComposable Files:
useShopProducts.ts, useShopOrders.ts (layer prefix + collection name)useProductHelpers.ts, useOrderFilters.tsuseUtility Files:
formatters.ts, validators.ts, productHelpers.tsType Files:
products.ts, orders.ts, shared.tsLayer directory names should:
products, layer is layers/products/layers/blogPosts/, layers/orderItems/layers/shop/ for products, categories, ordersEvery generated layer follows this structure:
layers/[layer-name]/collections/[collection-name]/
├── app/
│ ├── components/
│ │ ├── _Form.vue # Generated form component
│ │ └── List.vue # Generated list component
│ │
│ └── composables/
│ └── use[LayerCollection].ts # Generated CRUD composable
│
├── server/
│ ├── api/teams/[id]/[layer]-[collection]/
│ │ ├── index.get.ts # List endpoint
│ │ ├── index.post.ts # Create endpoint
│ │ ├── [collectionId].patch.ts # Update endpoint (PATCH)
│ │ └── [collectionId].delete.ts # Delete endpoint
│ │
│ └── database/
│ └── schema.ts # Drizzle schema
│
├── types.ts # TypeScript types
└── nuxt.config.ts # Layer config
Place custom components in organized subdirectories:
layers/shop/collections/products/app/components/
├── _Form.vue # Generated
├── List.vue # Generated
├── fields/ # Custom field components
│ ├── PriceField.vue
│ ├── StockField.vue
│ └── CategoryField.vue
└── cards/ # Custom card layouts
├── ProductCard.vue
└── ProductCardMini.vue
Schema files should be organized in a schemas/ directory using JSON format:
schemas/
├── products.json # Single collection schema
├── categories.json
├── shop/ # Domain-grouped schemas
│ ├── products.json
│ ├── orders.json
│ └── orderItems.json
└── blog/
├── posts.json
├── authors.json
└── tags.json
{
"title": {
"type": "string",
"meta": { "required": true, "label": "Product Name" }
},
"price": {
"type": "number",
"meta": { "required": true }
},
"categoryId": {
"type": "string",
"refTarget": "categories"
}
}
NEVER define these fields in your schema - they're auto-generated:
id - Always generated (UUID or nanoid)teamId - Always generated (team-scoped by default)owner - Always generated (team-scoped by default)createdAt - Generated when useMetadata: true (default)updatedAt - Generated when useMetadata: true (default)createdBy - Generated when useMetadata: true (default)updatedBy - Generated when useMetadata: true (default)Common Mistake: Defining auto-generated fields in your schema causes duplicate key errors during build.
// ❌ BAD - These are auto-generated!
{
"id": { "type": "string" },
"createdAt": { "type": "date" },
"teamId": { "type": "string" }
}
// ✅ GOOD - Only define your custom fields
{
"title": { "type": "string" },
"description": { "type": "text" },
"price": { "type": "number" }
}
Text Fields:
{ "title": { "type": "string" } }
{ "description": { "type": "text" } }
Number Fields:
{ "quantity": { "type": "number" } }
{ "price": { "type": "decimal", "meta": { "precision": 10, "scale": 2 } } }
Boolean Fields:
{ "isActive": { "type": "boolean", "meta": { "default": true } } }
Date Fields:
{ "publishedAt": { "type": "date" } }
Reference Fields (use string type with refTarget):
{ "categoryId": { "type": "string", "refTarget": "categories" } }
Other Types: json, repeater, array, image, file
Override generated components by creating your own component with the same name in the collection directory. The generated _Form.vue and List.vue can be freely edited since they live in your project.
Component props follow Vue conventions:
modelValue, errorMessage, showModalmodel-value, error-message, show-modalisActive, hasError, canEditGenerated API endpoints follow REST conventions:
GET /api/teams/:teamId/shop-products # List all
POST /api/teams/:teamId/shop-products # Create new
PATCH /api/teams/:teamId/shop-products/:id # Update existing
DELETE /api/teams/:teamId/shop-products/:id # Delete
Standard query parameters:
page - Page number (1-indexed)limit - Items per pagesearch - Search querysort - Sort field (e.g., title, -createdAt for descending)filter - JSON filter objectExample:
GET /api/products?page=2&limit=20&search=laptop&sort=-price
Product, BlogPost, OrderItemInput: ProductInput, CreateProductInputQuery: ProductQuery, ProductsQueryResponse: ProductResponse, ProductsResponseExample:
// Collection type
export interface Product {
id: string
title: string
price: number
createdAt: Date
}
// Input type
export interface CreateProductInput {
title: string
price: number
}
// Query type
export interface ProductsQuery {
page?: number
limit?: number
search?: string
}
// Response type
export interface ProductsResponse {
items: Product[]
total: number
page: number
}
// ✅ Good - Organized imports
import { ref, computed } from 'vue'
import type { Product } from '~/layers/shop/collections/products/types'
import { useProducts } from '~/layers/shop/collections/products/app/composables/useShopProducts'
// ❌ Bad - Mixed order, no type imports
import { useProducts } from '~/layers/shop/collections/products/app/composables/useShopProducts'
import { ref, computed } from 'vue'
import { Product } from '~/layers/shop/collections/products/types' // Should be type import
Components should follow this order:
<!-- 1. Script setup -->
<script setup lang="ts">
// 1. Imports
import { ref, computed } from 'vue'
import type { Product } from '~/types'
// 2. Props
interface Props {
product: Product
}
const props = defineProps<Props>()
// 3. Emits
const emit = defineEmits<{
'update': [product: Product]
}>()
// 4. Composables
const { mutate } = useCroutonMutate()
// 5. Reactive state
const isEditing = ref(false)
// 6. Computed
const displayPrice = computed(() => `$${props.product.price.toFixed(2)}`)
// 7. Methods
const handleSave = async () => {
// ...
}
</script>
<!-- 2. Template -->
<template>
<!-- Component markup -->
</template>
<!-- 3. Styles (if needed) -->
<style scoped>
/* Component styles */
</style>
// layers/products/composables/useProducts.ts
import { ref } from 'vue'
import type { Product } from '~/types'
export const useProducts = () => {
// 1. State
const products = ref<Product[]>([])
const loading = ref(false)
// 2. Methods
const fetchProducts = async () => {
loading.value = true
try {
products.value = await $fetch('/api/products')
} finally {
loading.value = false
}
}
// 3. Return public API
return {
products: readonly(products),
loading: readonly(loading),
fetchProducts
}
}
id, createdAt, etc.