Skip to content

Cookbook: Simple CRUD Web App ​

In traditional frameworks, building a CRUD (Create, Read, Update, Delete) API is a tedious rite of passage. You have to set up a router, define an ORM schema, write controller functions, wire up validation middleware, and write boilerplate tests.

In Carotene, CRUD is almost entirely declarative. You define the data shape, lock down the security boundaries, and use the Generative Operator (@) to handle the annoying data-formatting tasks.

Let's build a fully secure, tested headless CMS for a company blog in a single file.

1. The Architecture Blueprint ​

We start by defining our domain, our data shape, and the physical database store.

dart
domain Content {
  
  // 1. The Data Shape
  model Post {
    id: UUID
    title: String
    slug: String
    content: String
    authorId: UUID
    isPublished: Boolean
    createdAt: Date
  }

  // 2. The Physical Storage
  store Post {
    model: Post
  }
}

2. The Backend Endpoints ​

Next, we define the backend block containing our four CRUD operations. Notice how we use the Zero-Trust verbs (creates, reads, updates, deletes) to strictly bound what each endpoint is allowed to do.

dart
backend CMSApi {

  // ==========================================
  // CREATE
  // ==========================================
  function CreatePost(title: store.Post.title, content: store.Post.content) {
    requires session.isAuthenticated
    creates store.Post
    
    implements {
      // Use the Generative Operator to handle string manipulation
      slug = @("Convert the ${title} into a URL-friendly lowercase slug, replacing spaces with hyphens and removing special characters.")
      
      newPost = store.Post {
        id: Crypto.generateUUID()
        title: title
        slug: slug
        content: content
        authorId: session.userId
        isPublished: false
        createdAt: Time.now()
      }
      
      return newPost
    }
  }

  // ==========================================
  // READ
  // ==========================================
  function GetPost(targetSlug: store.Post.slug) {
    reads store.Post
    
    implements {
      // Anyone can read, but we must ensure it exists and is published
      targetPost = store.Post.findBy(slug: targetSlug)
      
      if (!targetPost.exists()) {
        throws "Post not found"
      }
      
      if (targetPost.isPublished == false && session.role != "Admin") {
        throws "You do not have permission to view drafts"
      }
      
      return targetPost
    }
  }

  // ==========================================
  // UPDATE
  // ==========================================
  function UpdatePostContent(postId: store.Post.id, newContent: store.Post.content) {
    requires session.isAuthenticated
    updates store.Post
    
    implements {
      targetPost = store.Post.find(postId)
      
      // Only the author or an Admin can edit the post
      if (targetPost.authorId != session.userId && session.role != "Admin") {
        throws "Unauthorized to edit this post"
      }
      
      targetPost.content = newContent
      return targetPost
    }
  }

  // ==========================================
  // DELETE
  // ==========================================
  function DeletePost(postId: store.Post.id) {
    requires session.role == "Admin" // Strict override: Only Admins can delete
    deletes store.Post
    
    implements {
      store.Post.find(postId).delete()
      return true
    }
  }
}

3. The Proof (Business Logic Tests) ​

Because we used the Generative Operator to handle the URL slug generation in CreatePost, we need to write a test to mathematically guarantee the AI generates the string formatting code correctly.

We will also test our RBAC (Role-Based Access Control) to ensure our security logic holds up in the Vapor Sandbox.

dart
// Test 1: AI Slug Generation
test "Generates a clean URL slug on creation" {
  // 1. Mock an active user session
  given mock session { userId: "user_123", isAuthenticated: true }
  
  // 2. Trigger the action
  result = CreatePost("Hello World! Welcome to 2026.", "This is the content.")
  
  // 3. Assert the AI correctly synthesized the slugification logic
  asserts result.slug == "hello-world-welcome-to-2026"
  asserts store.Post.find(result.id).exists()
}

// Test 2: Security Validation
test "Blocks anonymous users from reading unpublished drafts" {
  // 1. Mock an unauthenticated session
  given mock session { isAuthenticated: false, role: "Guest" }
  
  // 2. Mock a draft post in the database
  given mock store.Post { 
    id: "post_1", 
    slug: "secret-draft", 
    isPublished: false 
  }
  
  // 3. Assert the endpoint physically rejects the request
  asserts GetPost("secret-draft") throws "You do not have permission to view drafts"
}

4. Compile and Deploy ​

With exactly 85 lines of highly readable text, you have architected a production-ready microservice.

When you run carrot build:

  1. The CLI provisions the database schema for Post.
  2. It generates a perfectly typed REST/tRPC router for CMSApi.
  3. It prompts the AI to write the Regex/string-manipulation code for the slug.
  4. It spins up the Sandbox, runs the tests, verifies the slug generation, and ensures the security rules return 403 errors when violated.

The output is zero-dependency, highly optimized TypeScript or Go code resting in your .generated/ folder, ready to be deployed.