Reference

Conventions

Naming conventions, file organization, and coding standards for Nuxt Crouton

This guide documents the conventions used throughout Nuxt Crouton. Following these conventions ensures consistency, predictability, and easier collaboration.

Naming Conventions

Collection Names

Collections should follow these rules:

  • Use plural names: products, users, orders
  • Use camelCase for multi-word names: blogPosts, orderItems, userProfiles
  • Be descriptive: userProfiles is better than just profiles
  • Avoid abbreviations: categories not cats
  • Avoid generic names: Don't use items, data, records

Examples:

# ✅ Good
name: products
name: blogPosts
name: orderItems

# ❌ Bad
name: product      # Singular
name: blog_posts   # snake_case
name: items        # Too generic
name: prods        # Abbreviated

Field Names

Field names should:

  • Use camelCase: firstName, createdAt, isActive
  • Be descriptive: emailAddress not email if you also have emailVerified
  • Use conventional names:
    • Dates: createdAt, updatedAt, publishedAt, deletedAt
    • Booleans: isActive, hasAccess, canEdit
    • Relationships: userId, categoryId, authorId

Examples:

{
  "firstName": { "type": "string" },        // ✅ Good
  "isPublished": { "type": "boolean" },     // ✅ Good
  "publishedAt": { "type": "date" },        // ✅ Good
  "authorId": { "type": "reference" },      // ✅ 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": "reference" }         // ❌ Bad: should be authorId
}

File Names

Generated and custom files follow these patterns:

Component Files:

  • Generated components: CroutonForm.vue, CroutonTable.vue, CroutonModal.vue
  • Custom components: ProductCard.vue, OrderStatusBadge.vue, UserAvatar.vue
  • Use PascalCase for all Vue components

Composable Files:

  • Generated composables: useProducts.ts, useOrders.ts
  • Custom composables: useProductHelpers.ts, useOrderFilters.ts
  • Use camelCase starting with use

Utility Files:

  • Use camelCase: formatters.ts, validators.ts, productHelpers.ts

Type Files:

  • Use camelCase: products.ts, orders.ts, shared.ts

Layer Names

Layer directory names should:

  • Match collection names: If collection is products, layer is layers/products/
  • Use plural, camelCase: layers/blogPosts/, layers/orderItems/
  • Group by domain for multiple collections: layers/shop/ for products, categories, orders

File Organization

Standard Layer Structure

Every generated layer follows this structure:

layers/[collection-name]/
├── components/
│   ├── CroutonForm.vue          # Generated form component
│   ├── CroutonTable.vue         # Generated table component
│   ├── CroutonModal.vue         # Generated modal component
│   └── custom/                  # Your custom components
│       ├── CustomField.vue
│       └── CustomColumn.vue
│
├── composables/
│   ├── use[Collection].ts       # Generated CRUD composable
│   ├── use[Collection]Form.ts   # Generated form composable
│   ├── use[Collection]Table.ts  # Generated table composable
│   └── use[Collection]Helpers.ts # Your custom composable
│
├── server/
│   ├── api/
│   │   └── [collection]/
│   │       ├── index.get.ts     # List endpoint
│   │       ├── index.post.ts    # Create endpoint
│   │       ├── [id].get.ts      # Get single item
│   │       ├── [id].put.ts      # Update endpoint
│   │       └── [id].delete.ts   # Delete endpoint
│   │
│   └── database/
│       └── schema/
│           └── [collection].ts   # Drizzle schema
│
├── types/
│   └── [collection].ts           # TypeScript types
│
├── utils/
│   └── [collection]Helpers.ts    # Utility functions
│
├── pages/ (optional)
│   └── [collection]/
│       ├── index.vue             # List page
│       └── [id].vue              # Detail page
│
├── nuxt.config.ts                # Layer config
└── package.json                  # Layer dependencies

Custom Components Placement

Place custom components in organized subdirectories:

layers/products/components/
├── CroutonForm.vue              # Generated
├── CroutonTable.vue             # Generated
├── fields/                       # Custom field components
│   ├── PriceField.vue
│   ├── StockField.vue
│   └── CategoryField.vue
├── columns/                      # Custom column renderers
│   ├── PriceColumn.vue
│   └── StatusColumn.vue
└── cards/                        # Custom card layouts
    ├── ProductCard.vue
    └── ProductCardMini.vue

Schema Files

Schema files should be organized in the collections/ directory:

collections/
├── products.yml         # Single collection schema
├── categories.yml
├── shop/                # Domain-grouped schemas
│   ├── products.yml
│   ├── orders.yml
│   └── orderItems.yml
└── blog/
    ├── posts.yml
    ├── authors.yml
    └── tags.yml

Collection Schema Patterns

Basic Schema Structure

name: products              # Collection name (plural, camelCase)
description: Product catalog # Human-readable description
icon: i-heroicons-shopping-bag # Heroicon identifier

fields:
  - name: title             # Field name (camelCase)
    type: text              # Field type
    required: true          # Validation
    label: Product Name     # Display label

  - name: price
    type: number
    validation:
      min: 0
      max: 999999

  - name: categoryId
    type: reference
    ref-target: categories  # Target collection

Auto-Generated Fields

NEVER define these fields in your schema - they're auto-generated:

  • id - Always generated (UUID or nanoid)
  • createdAt - Generated when useMetadata: true (default)
  • updatedAt - Generated when useMetadata: true (default)
  • updatedBy - Generated when useMetadata: true (default)
  • teamId - Generated when useTeamUtility: true
  • userId - Generated when useTeamUtility: true

Common Mistake: Defining auto-generated fields in your schema causes duplicate key errors during build.

# ❌ BAD - These are auto-generated!
fields:
  - name: id
  - name: createdAt
  - name: teamId

# ✅ GOOD - Only define your custom fields
fields:
  - name: title
  - name: description
  - name: price

Field Type Conventions

Text Fields:

- name: title
  type: text       # Short text (< 255 chars)

- name: description
  type: longtext   # Long text (> 255 chars)

- name: content
  type: richtext   # Rich text with formatting

Number Fields:

- name: quantity
  type: integer    # Whole numbers

- name: price
  type: decimal    # Decimal numbers (2 decimal places)
  validation:
    min: 0

Boolean Fields:

- name: isActive
  type: boolean
  default: true

Date Fields:

- name: publishedAt
  type: date       # Date only

- name: eventAt
  type: datetime   # Date + time

- name: createdAt
  type: timestamp  # Unix timestamp

Reference Fields:

- name: categoryId
  type: reference
  ref-target: categories  # Target collection (plural)

- name: tagIds
  type: reference
  ref-target: tags
  multiple: true          # Many-to-many

Select Fields:

- name: status
  type: select
  options:
    - draft
    - published
    - archived
  default: draft

Component Conventions

Slot Naming

Custom slots follow these patterns:

Form Field Slots:

<template #field-[fieldName]="{ modelValue, updateModelValue, error }">
  <CustomField :model-value="modelValue" @update:model-value="updateModelValue" />
</template>

Table Column Slots:

<template #column-[fieldName]="{ row, value }">
  <CustomColumn :row="row" :value="value" />
</template>

Action Slots:

<template #actions="{ item }">
  <UButton @click="customAction(item)">Custom</UButton>
</template>

Props Naming

Component props follow Vue conventions:

  • Use camelCase in script: modelValue, errorMessage, showModal
  • Use kebab-case in templates: model-value, error-message, show-modal
  • Prefix boolean props: isActive, hasError, canEdit

API Endpoint Patterns

RESTful Conventions

Generated API endpoints follow REST conventions:

GET    /api/products          # List all
GET    /api/products/:id      # Get single
POST   /api/products          # Create new
PUT    /api/products/:id      # Update existing
DELETE /api/products/:id      # Delete

Query Parameters

Standard query parameters:

  • page - Page number (1-indexed)
  • limit - Items per page
  • search - Search query
  • sort - Sort field (e.g., title, -createdAt for descending)
  • filter - JSON filter object

Example:

GET /api/products?page=2&limit=20&search=laptop&sort=-price

TypeScript Conventions

Type Naming

  • Collection types: PascalCase matching collection: Product, BlogPost, OrderItem
  • Input types: Suffix with Input: ProductInput, CreateProductInput
  • Query types: Suffix with Query: ProductQuery, ProductsQuery
  • Response types: Suffix with Response: ProductResponse, ProductsResponse

Example:

// 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
}

Import Conventions

// ✅ Good - Organized imports
import { ref, computed } from 'vue'
import type { Product } from '~/layers/products/types/products'
import { useProducts } from '~/layers/products/composables/useProducts'

// ❌ Bad - Mixed order, no type imports
import { useProducts } from '~/layers/products/composables/useProducts'
import { ref, computed } from 'vue'
import { Product } from '~/layers/products/types/products' // Should be type import

Code Style

Vue Component Structure

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>

Composable Structure

// 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
  }
}

Best Practices Summary

  1. Follow naming conventions: Plural collections, camelCase fields, PascalCase components
  2. Organize files consistently: Use standard layer structure
  3. Never define auto-generated fields: Let Nuxt Crouton add id, createdAt, etc.
  4. Use TypeScript: Type everything for safety and better DX
  5. Follow Vue conventions: Component structure, prop naming, import order
  6. Keep customizations separate: Custom components in subdirectories
  7. Use descriptive names: Avoid abbreviations and generic names