Loopar Introduction
loopar-webpage

Learn Loopar

Welcome to the Loopar tutorials. These hands-on guides will take you from zero to building complete applications.


Learning Path

TutorialTimeWhat You'll Build
1. Your First App5 minA complete app structure
2. Creating Entities15 minA Task Manager with CRUD
3. Visual Page Design15 minA landing page with drag & drop
4. Custom Code20 minBusiness logic and custom UI
5. Working with Data15 minQueries, filters, and relationships
6. Forms & Validation15 minCustom forms with validation
7. Deploy to Production10 minCreate tenant and go live

Prerequisites

Before starting, make sure you have:

  • āœ… Loopar installed and running (npx loopar-install my-project)
  • āœ… Access to http://localhost:3000
  • āœ… Completed the setup wizard
  • āœ… Logged into the Desk

How to Use These Tutorials

Each tutorial:

  1. States the goal — What you'll build
  2. Lists the steps — Follow along in your Desk
  3. Shows the result — What you should see
  4. Explains why — Understanding the concepts

Tip: Keep your Loopar Desk open in one tab and this documentation in another. Follow along step by step.

Let's start building!

Tutorial 1: Your First App

Time: 5 minutes
Goal: Create your first Loopar application


What is an App?

An App in Loopar is a container for your modules and entities. Think of it as a project folder that groups related functionality.


Step 1: Open the Desk

Navigate to:

http://localhost:3000/desk

You should see the Desk interface with a sidebar menu.


Step 2: Create a New App

  1. In the sidebar, go to Core → App
  2. Click the + New button
  3. Fill in the form:
FieldValue
Nametask-manager
DescriptionA simple task management application
  1. Click Save

Step 3: What Just Happened?

Loopar created:

/apps/
└── task-manager/
    ā”œā”€ā”€ app.json              # App metadata
    └── modules/
        └── task-manager/     # Default module (same name as app)
  • āœ… A new app directory
  • āœ… The app.json configuration file
  • āœ… A default module with the same name

Step 4: Verify Your App

  1. Go to Core → App in the sidebar
  2. You should see task-manager in the list
  3. Click on it to view its details

What You Learned

  • Apps are the top-level containers in Loopar
  • Creating an app automatically creates a default module
  • All your entities will belong to this app through its modules

Next Step

Now that you have an app, let's create your first Entity to store data.

→ Continue to Tutorial 2: Creating Entities

Tutorial 2: Creating Entities

Time: 15 minutes
Goal: Create a Task entity with full CRUD functionality


What We're Building

A Task entity with:

  • Title, description, status
  • Due date and priority
  • Auto-generated list view and form

Step 1: Create the Entity

  1. Go to Core → Entity
  2. Click + New
  3. Fill in the basic info:
FieldValue
NameTask
Moduletask-manager
Is SingleāŒ No (we want multiple tasks)

Step 2: Add Fields (Drag & Drop)

Now let's add fields to our Task. In the form designer:

Add a Title field

  1. From the components panel, drag Data into the form area
  2. Click on it and set:
    • Name: title
    • Label: Title
    • Required: āœ… Yes

Add a Description field

  1. Drag Text into the form
  2. Configure:
    • Name: description
    • Label: Description

Add a Status field

  1. Drag Select into the form
  2. Configure:
    • Name: status
    • Label: Status
    • Options: Open\nIn Progress\nCompleted\nCancelled
    • Default: Open

Add a Due Date field

  1. Drag Date into the form
  2. Configure:
    • Name: due_date
    • Label: Due Date

Add a Priority field

  1. Drag Select into the form
  2. Configure:
    • Name: priority
    • Label: Priority
    • Options: Low\nMedium\nHigh\nUrgent
    • Default: Medium

Step 3: Save the Entity

Click Save.

Loopar now creates:

  • āœ… Database table Task
  • āœ… REST API endpoints
  • āœ… List view with filters
  • āœ… Form for create/edit
  • āœ… Sequelize model

Step 4: Test Your Entity

View the List

  1. In the sidebar, find Task Manager → Task
  2. You should see an empty list view

Create a Task

  1. Click + New
  2. Fill in the form:
    • Title: Learn Loopar
    • Description: Complete all tutorials
    • Status: In Progress
    • Due Date: (pick a date)
    • Priority: High
  3. Click Save

View in List

  1. Go back to the list
  2. Your task appears with all fields
  3. Try the search and filters!

Step 5: Explore the Generated Features

Automatic Features

FeatureHow to Access
SearchType in the search box
FiltersClick filter icon, select status
PaginationBottom of the list
EditClick on any task
DeleteSelect task → Delete

API Endpoints (Automatic)

# List all tasks
curl http://localhost:3000/api/Task

# Get single task
curl http://localhost:3000/api/Task/TASK-0001

# Create task
curl -X POST http://localhost:3000/api/Task \
  -H "Content-Type: application/json" \
  -d '{"title": "New Task", "status": "Open"}'

What You Learned

  • Entities define data models visually
  • Drag & drop to add fields
  • Loopar generates everything: database, API, UI
  • Each entity gets a full CRUD interface

Challenge

Try adding more fields:

  • Assigned To (Data field)
  • Is Completed (Check field)
  • Estimated Hours (Float field)

→ Continue to Tutorial 3: Visual Page Design

Tutorial 3: Visual Page Design

Time: 15 minutes
Goal: Create a landing page using drag & drop


What We're Building

A landing page for our Task Manager app with:

  • Hero section with title and CTA
  • Features section
  • Call to action

Step 1: Create a New Page

  1. Go to Core → Page
  2. Click + New
  3. Fill in:
FieldValue
Nametask-manager-home
Moduletask-manager
TitleTask Manager - Get Things Done

Step 2: Build the Hero Section

Add a Section

  1. From components, drag Section into the page
  2. This is your full-width container

Add a Row and Columns

  1. Inside the section, drag a Row
  2. Configure layout: [50, 50] (two equal columns)

Left Column - Text Content

  1. Click on the left column

  2. Drag a Title into it:

    • Text: Manage Tasks Like a Pro
    • Class: display-4 mb-4
  3. Drag a Paragraph below:

    • Text: Simple, powerful task management for teams and individuals. Stay organized, meet deadlines, achieve goals.
    • Class: text-muted text-lg mb-6
  4. Drag a Link (button):

    • Label: Get Started Free
    • To: /desk/Task/list
    • Variant: default

Right Column - Image

  1. Click on the right column
  2. Drag an Image:
    • Background Image: (paste a URL or upload)
    • Aspect Ratio: 16:9
    • Background Size: contain

Step 3: Build the Features Section

Add Another Section

  1. Drag a new Section below the hero
  2. Add a Text Block centered:
    • Subtitle: Features
    • Title: Everything you need to stay productive

Add Feature Cards

  1. Drag a Row with layout [33, 33, 33]
  2. In each column, drag a Feature Card:

Card 1:

  • Icon: CheckSquare
  • Title: Task Tracking
  • Description: Create, organize, and track tasks with ease

Card 2:

  • Icon: Calendar
  • Title: Due Dates
  • Description: Never miss a deadline with smart reminders

Card 3:

  • Icon: Users
  • Title: Team Collaboration
  • Description: Work together seamlessly with your team

Step 4: Add a CTA Section

  1. Drag a new Section

  2. Add a Panel with:

    • Variant: gradient
    • Class: text-center p-12
  3. Inside the panel:

    • Title: Ready to get organized?
    • Paragraph: Start managing your tasks today. It's free.
    • Link: Start Now → /desk/Task/new

Step 5: Save and Preview

  1. Click Save
  2. Open a new tab: http://localhost:3000/task-manager-home
  3. See your landing page live!

Step 6: Add Animations (Optional)

Make it fancy:

  1. Select any element
  2. In properties, set:
    • Animation: fade-up, fade-right, zoom-in
    • Animation Delay: 100, 200, 300 (stagger effect)

Page Structure Recap

Page: task-manager-home
ā”œā”€ā”€ Section (Hero)
│   └── Row [50, 50]
│       ā”œā”€ā”€ Col: Title + Paragraph + Link
│       └── Col: Image
ā”œā”€ā”€ Section (Features)
│   ā”œā”€ā”€ Text Block (centered header)
│   └── Row [33, 33, 33]
│       ā”œā”€ā”€ Feature Card 1
│       ā”œā”€ā”€ Feature Card 2
│       └── Feature Card 3
└── Section (CTA)
    └── Panel
        └── Title + Paragraph + Link

What You Learned

  • Pages are built with Sections, Rows, and Columns
  • Drag & drop any component
  • Configure properties for styling
  • Pages get automatic routes (/page-name)
  • Animations add polish

Challenge

Add more to your page:

  • A testimonials section
  • A pricing table
  • A contact form

→ Continue to Tutorial 4: Custom Code

Tutorial 4: Custom Code

Time: 20 minutes
Goal: Add business logic and custom UI to your entity


When You Need Custom Code

The visual builder handles 80% of cases. For the rest:

  • Model (.js): Business logic, hooks, helper methods (NOT URL accessible)
  • Controller (-controller.js): API actions (URL accessible via action* methods)
  • Client (.jsx): Custom UI, interactions, real-time updates

Understanding Model vs Controller

FilePurposeURL Accessible
task.jsBusiness logic, hooks, data methodsāŒ No
task-controller.jsAPI endpointsāœ… Yes (only action* methods)
task.jsxReact UI component— (client-side)

Part A: Server-Side Logic

Step 1: Locate Your Entity Files

Your Task entity files are at:

/apps/task-manager/modules/task-manager/task/
ā”œā”€ā”€ task.json              # Field definitions (already exists)
ā”œā”€ā”€ task.js                # Model - business logic (create this)
ā”œā”€ā”€ task-controller.js     # Controller - API actions (create this)
└── task.jsx               # Client-side React (create this)

Step 2: Create the Model

Create task.js — this handles business logic and data:

import { BaseDocument } from "loopar";

export default class Task extends BaseDocument {
  constructor(props) {
    super(props);
  }

  // ═══════════════════════════════════════
  // HELPER METHODS (not URL accessible)
  // ═══════════════════════════════════════
  
  getFullTitle() {
    return `[${this.priority}] ${this.title}`;
  }

  isOverdue() {
    if (!this.due_date || this.status === "Completed") return false;
    return new Date(this.due_date) < new Date();
  }

  getDaysUntilDue() {
    if (!this.due_date) return null;
    const diff = new Date(this.due_date) - new Date();
    return Math.ceil(diff / (1000 * 60 * 60 * 24));
  }

  // ═══════════════════════════════════════
  // VALIDATION
  // ═══════════════════════════════════════
  
  async validate() {
    if (this.title && this.title.length < 3) {
      throw new Error("Title must be at least 3 characters");
    }

    if (this.isNew && this.due_date) {
      const dueDate = new Date(this.due_date);
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      
      if (dueDate < today) {
        throw new Error("Due date cannot be in the past");
      }
    }

    if (this.priority === "Urgent" && !this.due_date) {
      throw new Error("Urgent tasks must have a due date");
    }
  }

  // ═══════════════════════════════════════
  // LIFECYCLE HOOKS
  // ═══════════════════════════════════════
  
  async beforeInsert() {
    this.created_date = new Date();
    if (!this.status) {
      this.status = "Open";
    }
  }

  async beforeSave() {
    // Auto-set completed_date when status changes to Completed
    if (this.status === "Completed" && !this.completed_date) {
      this.completed_date = new Date();
    }
    
    // Clear completed_date if reopened
    if (this.status !== "Completed") {
      this.completed_date = null;
    }
  }

  async afterSave() {
    console.log(`Task ${this.name} saved with status: ${this.status}`);
  }
}

Step 3: Create the Controller

Create task-controller.js — only action* methods are URL accessible:

import { BaseController } from "loopar";

export default class TaskController extends BaseController {
  
  // ═══════════════════════════════════════
  // URL ACCESSIBLE ACTIONS
  // Only methods starting with 'action' are accessible via URL
  // ═══════════════════════════════════════

  // GET/POST /api/Task/view
  async actionView() {
    const task = await loopar.getDocument("Task", this.name);
    
    return {
      ...task,
      fullTitle: task.getFullTitle(),
      isOverdue: task.isOverdue(),
      daysUntilDue: task.getDaysUntilDue()
    };
  }

  // POST /api/Task/mark-complete
  async actionMarkComplete() {
    const task = await loopar.getDocument("Task", this.data.name);
    task.status = "Completed";
    task.completed_date = new Date();
    await task.save();
    
    return {
      success: true,
      message: `Task "${task.title}" marked as complete`
    };
  }

  // POST /api/Task/mark-urgent
  async actionMarkUrgent() {
    const task = await loopar.getDocument("Task", this.data.name);
    task.priority = "Urgent";
    await task.save();
    
    return {
      success: true,
      message: `Task "${task.title}" marked as urgent`
    };
  }

  // GET /api/Task/stats
  async actionStats() {
    const open = await loopar.db.count("Task", { status: "Open" });
    const inProgress = await loopar.db.count("Task", { status: "In Progress" });
    const completed = await loopar.db.count("Task", { status: "Completed" });
    
    return {
      open,
      inProgress,
      completed,
      total: open + inProgress + completed
    };
  }

  // GET /api/Task/overdue
  async actionOverdue() {
    const tasks = await loopar.db.getAll("Task", {
      filters: {
        status: ["Open", "In Progress"]
      }
    });

    const overdue = tasks.filter(t => {
      if (!t.due_date) return false;
      return new Date(t.due_date) < new Date();
    });

    return { overdue };
  }

  // ═══════════════════════════════════════
  // PRIVATE HELPER METHODS (NOT URL accessible)
  // Methods without 'action' prefix are internal only
  // ═══════════════════════════════════════
  
  formatTaskForResponse(task) {
    return {
      name: task.name,
      title: task.title,
      status: task.status
    };
  }
}

Step 4: Test Your Custom Logic

  1. Restart your dev server (or wait for hot reload)
  2. Try creating a task with a 2-character title → Error!
  3. Try setting a past due date → Error!
  4. Create a task, then set status to Completed → completed_date auto-fills

Test Controller Actions

# Get stats (accessible because it starts with 'action')
curl http://localhost:3000/api/Task/stats

# Mark a task complete
curl -X POST http://localhost:3000/api/Task/mark-complete \
  -H "Content-Type: application/json" \
  -d '{"name": "TASK-0001"}'

# Get overdue tasks
curl http://localhost:3000/api/Task/overdue

Part B: Client-Side UI

Step 1: Create the React Component

Create task.jsx:

import { BaseForm, useDocument } from "@loopar/components";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { CheckCircle, AlertTriangle } from "lucide-react";

export default function TaskForm(props) {
  const { document, setValue, save, loading } = useDocument();

  // ═══════════════════════════════════════
  // QUICK ACTIONS
  // ═══════════════════════════════════════
  
  const handleMarkComplete = async () => {
    setValue("status", "Completed");
    await save();
  };

  const handleMarkUrgent = async () => {
    setValue("priority", "Urgent");
    await save();
  };

  // ═══════════════════════════════════════
  // STATUS BADGE
  // ═══════════════════════════════════════
  
  const getStatusColor = (status) => {
    const colors = {
      "Open": "bg-blue-500",
      "In Progress": "bg-yellow-500",
      "Completed": "bg-green-500",
      "Cancelled": "bg-gray-500"
    };
    return colors[status] || "bg-gray-500";
  };

  // ═══════════════════════════════════════
  // RENDER
  // ═══════════════════════════════════════
  
  return (
    <BaseForm {...props}>
      {/* Custom Header */}
      <div className="flex items-center justify-between mb-6 p-4 bg-secondary rounded-lg">
        <div className="flex items-center gap-3">
          <h2 className="text-xl font-semibold">{document.title || "New Task"}</h2>
          {document.status && (
            <Badge className={getStatusColor(document.status)}>
              {document.status}
            </Badge>
          )}
          {document.priority === "Urgent" && (
            <Badge variant="destructive" className="flex items-center gap-1">
              <AlertTriangle className="w-3 h-3" />
              Urgent
            </Badge>
          )}
        </div>
        
        {/* Quick Actions */}
        <div className="flex gap-2">
          {document.status !== "Completed" && (
            <Button 
              variant="outline" 
              size="sm"
              onClick={handleMarkComplete}
              disabled={loading}
            >
              <CheckCircle className="w-4 h-4 mr-2" />
              Mark Complete
            </Button>
          )}
          {document.priority !== "Urgent" && (
            <Button 
              variant="destructive" 
              size="sm"
              onClick={handleMarkUrgent}
              disabled={loading}
            >
              <AlertTriangle className="w-4 h-4 mr-2" />
              Mark Urgent
            </Button>
          )}
        </div>
      </div>

      {/* Due Date Warning */}
      {document.due_date && new Date(document.due_date) < new Date() && document.status !== "Completed" && (
        <div className="mb-4 p-3 bg-destructive/10 border border-destructive rounded-lg text-destructive">
          āš ļø This task is overdue!
        </div>
      )}

      {/* The rest of the form renders automatically */}
    </BaseForm>
  );
}

Step 2: See It in Action

  1. Go to Task Manager → Task
  2. Click on any task or create a new one
  3. You'll see your custom header, badges, and quick action buttons!

Summary: Model vs Controller

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                    SERVER-SIDE ARCHITECTURE                 │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                             │
│   task.js (MODEL)                                           │
│   ā”œā”€ā”€ getFullTitle()      ← Helper method (internal)        │
│   ā”œā”€ā”€ isOverdue()         ← Helper method (internal)        │
│   ā”œā”€ā”€ validate()          ← Lifecycle hook                  │
│   ā”œā”€ā”€ beforeSave()        ← Lifecycle hook                  │
│   └── afterSave()         ← Lifecycle hook                  │
│                                                             │
│   task-controller.js (CONTROLLER)                           │
│   ā”œā”€ā”€ actionView()        ← GET /api/Task/view              │
│   ā”œā”€ā”€ actionMarkComplete()← POST /api/Task/mark-complete    │
│   ā”œā”€ā”€ actionStats()       ← GET /api/Task/stats             │
│   └── formatTask()        ← Private helper (no URL access)  │
│                                                             │
│   RULE: Only methods starting with 'action' are URL routes  │
│                                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

What You Learned

  • Model (.js): Business logic, hooks, helper methods — NOT URL accessible
  • Controller (-controller.js): Only action* methods are URL accessible
  • Client (.jsx): Custom React UI
  • The controller calls model methods to get processed data

Code Location Summary

NeedFileURL Accessible
Validation rulesModel .jsāŒ No
Helper methodsModel .jsāŒ No
Lifecycle hooksModel .jsāŒ No
API endpointsController -controller.jsāœ… Only action*
Custom UI.jsx—

→ Continue to Tutorial 5: Working with Data

Tutorial 5: Working with Data

Time: 15 minutes
Goal: Query data, create relationships, and work with the database API


What We're Building

We'll extend our Task Manager with:

  • A Project entity (to group tasks)
  • Relationship between Project and Task
  • Queries to fetch related data

Step 1: Create the Project Entity

  1. Go to Core → Entity → + New
  2. Configure:
FieldValue
NameProject
Moduletask-manager
  1. Add fields:

    • name (Data, Required)
    • description (Text)
    • status (Select: Active, On Hold, Completed)
    • start_date (Date)
    • end_date (Date)
  2. Save the entity


  1. Open the Task entity for editing

  2. Add a new Link field:

    • Name: project
    • Label: Project
    • Options: Project
  3. Save the Task entity

Now each task can belong to a project!


Step 3: Create Project Model

Create project.js for project-specific logic:

import { BaseDocument } from "loopar";

export default class Project extends BaseDocument {
  
  // ═══════════════════════════════════════
  // HELPER METHODS
  // ═══════════════════════════════════════

  async getTaskCount() {
    return await loopar.db.count("Task", { project: this.name });
  }

  async getCompletedTaskCount() {
    return await loopar.db.count("Task", { 
      project: this.name, 
      status: "Completed" 
    });
  }

  async getProgress() {
    const total = await this.getTaskCount();
    if (total === 0) return 0;
    const completed = await this.getCompletedTaskCount();
    return Math.round((completed / total) * 100);
  }

  // ═══════════════════════════════════════
  // VALIDATION
  // ═══════════════════════════════════════
  
  async validate() {
    if (this.start_date && this.end_date) {
      if (new Date(this.end_date) < new Date(this.start_date)) {
        throw new Error("End date must be after start date");
      }
    }
  }
}

Step 4: Create Project Controller

Create project-controller.js:

import { BaseController } from "loopar";

export default class ProjectController extends BaseController {
  
  // GET /api/Project/tasks
  async actionTasks() {
    const tasks = await loopar.db.getAll("Task", {
      filters: { project: this.data.name },
      fields: ["name", "title", "status", "priority", "due_date"],
      orderBy: "priority DESC, due_date ASC"
    });

    return { 
      project: this.data.name,
      tasks 
    };
  }

  // GET /api/Project/stats
  async actionStats() {
    const project = await loopar.getDocument("Project", this.data.name);
    
    const tasks = await loopar.db.getAll("Task", {
      filters: { project: this.data.name }
    });

    const byStatus = {};
    const byPriority = {};

    tasks.forEach(task => {
      byStatus[task.status] = (byStatus[task.status] || 0) + 1;
      byPriority[task.priority] = (byPriority[task.priority] || 0) + 1;
    });

    // Calculate overdue tasks
    const today = new Date();
    const overdue = tasks.filter(t => 
      t.due_date && 
      new Date(t.due_date) < today && 
      t.status !== "Completed"
    ).length;

    return {
      project: project.name,
      total: tasks.length,
      progress: await project.getProgress(),
      byStatus,
      byPriority,
      overdue
    };
  }

  // POST /api/Project/check-completion
  async actionCheckCompletion() {
    const project = await loopar.getDocument("Project", this.data.name);
    
    const incompleteTasks = await loopar.db.count("Task", {
      project: this.data.name,
      status: ["Open", "In Progress"]
    });

    if (incompleteTasks === 0) {
      project.status = "Completed";
      await project.save();
      return { completed: true };
    }

    return { completed: false, remaining: incompleteTasks };
  }
}

Step 5: Database API Reference

Common database operations:

// ═══════════════════════════════════════
// GET DOCUMENTS
// ═══════════════════════════════════════

// Get single document by name
const task = await loopar.getDocument("Task", "TASK-0001");

// Get all documents
const allTasks = await loopar.db.getAll("Task");

// Get with filters
const urgentTasks = await loopar.db.getAll("Task", {
  filters: { 
    priority: "Urgent",
    status: ["Open", "In Progress"]  // OR condition
  },
  fields: ["name", "title", "due_date"],
  orderBy: "due_date ASC",
  limit: 10,
  offset: 0
});

// ═══════════════════════════════════════
// COUNT DOCUMENTS
// ═══════════════════════════════════════

const total = await loopar.db.count("Task");
const openCount = await loopar.db.count("Task", { status: "Open" });

// ═══════════════════════════════════════
// CREATE DOCUMENTS
// ═══════════════════════════════════════

const newTask = await loopar.newDocument("Task");
newTask.title = "New Task";
newTask.status = "Open";
newTask.project = "PROJECT-001";
await newTask.save();

// ═══════════════════════════════════════
// UPDATE DOCUMENTS
// ═══════════════════════════════════════

const task = await loopar.getDocument("Task", "TASK-0001");
task.status = "Completed";
await task.save();

// ═══════════════════════════════════════
// DELETE DOCUMENTS
// ═══════════════════════════════════════

const task = await loopar.getDocument("Task", "TASK-0001");
await task.delete();

What You Learned

  • Link fields create relationships between entities
  • Model has helper methods (not URL accessible)
  • Controller has action* methods (URL accessible)
  • loopar.getDocument() fetches a single document
  • loopar.db.getAll() queries multiple documents
  • loopar.db.count() counts matching documents

→ Continue to Tutorial 6: Forms & Validation

Tutorial 6: Forms & Validation

Time: 15 minutes
Goal: Create standalone forms with validation


When to Use Standalone Forms

  • Login/registration forms
  • Contact forms
  • Search forms
  • Multi-step wizards
  • Forms that don't map directly to an entity

Step 1: Create a Contact Form

  1. Go to Core → Form → + New
  2. Configure:
FieldValue
Namecontact-form
Moduletask-manager
TitleContact Us
  1. Add fields (drag & drop):

    • name (Data, Required, Label: "Your Name")
    • email (Email, Required, Label: "Email Address")
    • subject (Data, Required, Label: "Subject")
    • message (Text, Required, Label: "Message")
  2. Save the form


Step 2: Create the Form Controller

Create contact-form-controller.js to handle submission:

import { BaseController } from "loopar";

export default class ContactFormController extends BaseController {
  
  // POST /api/contact-form/submit
  async actionSubmit() {
    const { name, email, subject, message } = this.data;

    // Validation
    const errors = [];

    if (!name || name.length < 2) {
      errors.push("Name must be at least 2 characters");
    }

    if (!email || !this.isValidEmail(email)) {
      errors.push("Please provide a valid email address");
    }

    if (!subject || subject.length < 5) {
      errors.push("Subject must be at least 5 characters");
    }

    if (!message || message.length < 20) {
      errors.push("Message must be at least 20 characters");
    }

    if (errors.length > 0) {
      return {
        success: false,
        errors
      };
    }

    // Process the form
    await this.processContactForm({ name, email, subject, message });

    return {
      success: true,
      message: "Thank you for your message! We'll get back to you soon."
    };
  }

  // Private helper (no 'action' prefix = not URL accessible)
  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  // Private helper
  async processContactForm(data) {
    console.log("Contact form received:", data);
    // Save to database, send email, etc.
  }
}

Step 3: Entity-Level Validation

For entity forms, add validation in the Model .js file:

// task.js (MODEL)
export default class Task extends BaseDocument {
  
  async validate() {
    const errors = [];

    // Required
    if (!this.title) {
      errors.push("Title is required");
    }

    // Length
    if (this.title && this.title.length < 3) {
      errors.push("Title must be at least 3 characters");
    }

    // Business rules
    if (this.priority === "Urgent" && !this.due_date) {
      errors.push("Urgent tasks must have a due date");
    }

    if (errors.length > 0) {
      throw new Error(errors.join("\n"));
    }
  }
}

Validation Patterns

PatternModel (.js)Controller (-controller.js)
Entity validationāœ… validate() hook—
Form validationā€”āœ… In action* method
Business rulesāœ… Always—
API input validationā€”āœ… Always

What You Learned

  • Standalone forms use Form Builder + controller
  • Entity validation goes in the Model's validate() method
  • Form validation goes in the Controller's action* method
  • Only action* methods in controllers are URL accessible

→ Continue to Tutorial 7: Deploy to Production

Tutorial 7: Deploy to Production

Time: 10 minutes
Goal: Create a tenant and deploy your app to production


What We're Doing

  1. Create a new tenant for a "client"
  2. Install our Task Manager app
  3. Configure a domain
  4. Deploy to production with SSL

Step 1: Create a New Tenant

  1. In your dev site, go to Core → Tenant
  2. Click + New
  3. Configure:
FieldValue
Nameacme-corp
Port3001
Database TypeMySQL (or SQLite for testing)
Database Nameacme_corp_db
  1. Click Save

Loopar creates:

  • āœ… New directory: sites/acme-corp/
  • āœ… Environment file: sites/acme-corp/.env
  • āœ… PM2 process configuration

Step 2: Start the Tenant

  1. In the tenant list, find acme-corp
  2. Click Start
  3. The tenant starts on port 3001

Access it at:

http://localhost:3001

You'll see the setup wizard for this new tenant.


Step 3: Complete Tenant Setup

  1. Open http://localhost:3001
  2. Complete the setup wizard:
    • Database configuration
    • Admin account
    • Project info
  3. Log in to the tenant's Desk

Step 4: Install Your App

  1. In the tenant's Desk, go to Core → App Manager
  2. Find task-manager in the available apps
  3. Click Install

The app is now installed on this tenant with:

  • āœ… All entities (Task, Project)
  • āœ… All pages
  • āœ… All modules

Step 5: Configure Domain (Production)

For production deployment, you need:

  • A domain pointing to your server
  • Caddy running on the server

Update Tenant Domain

  1. Go back to your dev site
  2. Open Core → Tenant → acme-corp
  3. Set the domain:
FieldValue
Domaintasks.acme-corp.com
Productionāœ… Yes
  1. Click Save

Step 6: Deploy to Production

  1. In the tenant record, click Set on Production (or Deploy)
  2. Loopar will:
    • Register the domain with Caddy
    • Obtain SSL certificate (Let's Encrypt)
    • Restart tenant with NODE_ENV=production
    • Enable production optimizations

Step 7: Verify Deployment

# Check PM2 status
pm2 list

# Check Caddy routes
curl http://localhost:2019/config/apps/http/servers/srv0/routes

Your site is now live at https://tasks.acme-corp.com!


Congratulations! šŸŽ‰

You've completed all the Loopar tutorials!

You now know how to:

  • āœ… Create apps and modules
  • āœ… Build entities with CRUD
  • āœ… Design pages visually
  • āœ… Add custom business logic (Model vs Controller)
  • āœ… Query and relate data
  • āœ… Create forms with validation
  • āœ… Deploy to production

Key Architecture Recap

/entity-name/
ā”œā”€ā”€ entity-name.json        # Field definitions
ā”œā”€ā”€ entity-name.js          # MODEL: hooks, helpers (not URL accessible)
ā”œā”€ā”€ entity-name-controller.js # CONTROLLER: action* methods (URL accessible)
└── entity-name.jsx         # CLIENT: React component

Next Steps

Happy building! šŸš€