Real-World Recipes

Practical guides for implementing common features using Nuxt Auto CRUD.

This section provides complete "recipes" for common application features. These examples demonstrate how to combine schemas, permissions, and custom logic to build real-world functionality.

Recipe 1: Newsletter Subscription (Write-Only)

This recipe demonstrates how to create a simple "Subscribe" form where public users can add their email address, but cannot see who else has subscribed.

1. Define the Schema

Create a table to store email addresses.

// server/db/schema/subscribers.ts
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { systemFields } from './utils'

export const subscribers = sqliteTable('subscribers', {
  ...systemFields,
  email: text('email').notNull().unique(),
})

2. Configure Permissions

We want Public users to be able to Create (subscribe), but NOT List or Read (privacy). Admins or Managers can have full access.

Database Seed / Permission Setup:

  • Role: public
  • Resource: subscribers
  • Permissions: ['create']

3. The Result

  • POST /api/subscribers: Public users can submit { "email": "..." }.
  • GET /api/subscribers: Public users will receive 403 Forbidden (protecting your list).
  • Admin Dashboard: Admins can view and manage the full list of subscribers.

Recipe 2: Testimonials System (Approval Workflow)

This recipe demonstrates a "User-Generated Content" system where the public submits content, but it requires Admin approval before being displayed on the site.

1. Define the Schema

We include a status field to manage visibility.

// server/db/schema/testimonials.ts
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { systemFields } from './utils'

export const testimonials = sqliteTable('testimonials', {
  ...systemFields,
  // Default to 'inactive' so new submissions aren't immediately visible
  status: text('status', { enum: ['active', 'inactive'] }).default('inactive'),
  name: text('name').notNull(),
  role: text('role').notNull(),
  content: text('content').notNull(),
  avatar: text('avatar'),
  company: text('company'),
})

2. Configure Permissions

  • Role: public
  • Resource: testimonials
  • Permissions: ['create'] (and optionally ['read', 'list'] if you want them to see all testimonials, otherwise restrict this).

In our template, we allow ['create', 'read', 'list'] for simplicity, but we use a Custom Endpoint to serve only "Approved" testimonials to the landing page.

3. Custom Endpoint for "Active" Items

Since the generic /api/testimonials endpoint might be restricted or return all items (including inactive ones if public has list access), we create a specific endpoint for the frontend display.

// server/api/active-testimonials.get.ts
import { eq, desc } from 'drizzle-orm'
import { testimonials } from '../db/schema'
import { db } from 'hub:db'

export default defineEventHandler(async () => {
  // Fetch only 'active' testimonials
  return await db.select()
    .from(testimonials)
    .where(eq(testimonials.status, 'active'))
    .orderBy(desc(testimonials.createdAt))
    .limit(9)
    .all()
})

4. Frontend Integration

On your landing page, fetch from the custom endpoint:

<script setup lang="ts">
const { data: testimonials } = await useFetch('/api/active-testimonials')
</script>

And use the Auto CRUD Form for submissions:

<CrudCreateRow
  resource="testimonials"
  :schema="{
    resource: 'testimonials',
    fields: [
      { name: 'name', type: 'text', required: true },
      { name: 'role', type: 'text', required: true },
      { name: 'content', type: 'textarea', required: true }
    ]
  }"
/>

The CrudCreateRow component automatically handles the POST request to /api/testimonials.

Recipe 3: Multi-Role Registration

This recipe demonstrates how to allow users to select their role during registration (e.g., "Buyer" vs "Seller").

1. Setup Roles

First, ensure you have the roles defined in your roles table (e.g., "buyer", "seller").

2. Frontend Signup Form (Nuxt UI)

Update your signup form to include a role selection dropdown.

// app/pages/signup.vue

// 1. Fetch available roles
const { data: roles } = await useFetch<any[]>('/api/roles')

// 2. Filter internal roles and format for the dropdown
const selectableRoles = computed(() => {
  return roles.value
    ?.filter((r: any) => !['public', 'user', 'manager', 'admin'].includes(r.name))
    .map((r: any) => ({
      label: r.name ? r.name.charAt(0).toUpperCase() + r.name.slice(1) : 'Unknown',
      value: r.id
    })) || []
})

// 3. Add to Form Fields
const fields = computed(() => [
  // ... name, email, password
  {
    name: "roleId",
    type: "select",
    label: "Register As",
    items: selectableRoles.value,
  }
])

// 4. Handle Submission
// Nuxt UI selects might return an object instead of a value.
// Use a helper to extract the numeric ID.
async function onSubmit(payload: any) {
  const data = { ...payload.data };
  
  if (data.roleId && typeof data.roleId === 'object') {
    data.roleId = data.roleId.value;
  }

  await $fetch("/api/auth/signup", {
    method: "POST",
    body: data,
  });
  // ...
}

3. Backend Logic

The system's default signup endpoint (/api/auth/signup) accepts an optional roleId.

  • If provided, it verifies the role is valid and assigns it.
  • If not provided, it falls back to the default "user" role.