Learn Loopar
Welcome to the Loopar tutorials. These hands-on guides will take you from zero to building complete applications.
Learning Path
| Tutorial | Time | What You'll Build |
|---|---|---|
| 1. Your First App | 5 min | A complete app structure |
| 2. Creating Entities | 15 min | A Task Manager with CRUD |
| 3. Visual Page Design | 15 min | A landing page with drag & drop |
| 4. Custom Code | 20 min | Business logic and custom UI |
| 5. Working with Data | 15 min | Queries, filters, and relationships |
| 6. Forms & Validation | 15 min | Custom forms with validation |
| 7. Deploy to Production | 10 min | Create 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:
- States the goal ā What you'll build
- Lists the steps ā Follow along in your Desk
- Shows the result ā What you should see
- 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
- In the sidebar, go to Core ā App
- Click the + New button
- Fill in the form:
| Field | Value |
|---|---|
| Name | task-manager |
| Description | A simple task management application |
- 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
- Go to Core ā App in the sidebar
- You should see
task-managerin the list - 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
- Go to Core ā Entity
- Click + New
- Fill in the basic info:
| Field | Value |
|---|---|
| Name | Task |
| Module | task-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
- From the components panel, drag Data into the form area
- Click on it and set:
- Name:
title - Label:
Title - Required: ā Yes
- Name:
Add a Description field
- Drag Text into the form
- Configure:
- Name:
description - Label:
Description
- Name:
Add a Status field
- Drag Select into the form
- Configure:
- Name:
status - Label:
Status - Options:
Open\nIn Progress\nCompleted\nCancelled - Default:
Open
- Name:
Add a Due Date field
- Drag Date into the form
- Configure:
- Name:
due_date - Label:
Due Date
- Name:
Add a Priority field
- Drag Select into the form
- Configure:
- Name:
priority - Label:
Priority - Options:
Low\nMedium\nHigh\nUrgent - Default:
Medium
- Name:
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
- In the sidebar, find Task Manager ā Task
- You should see an empty list view
Create a Task
- Click + New
- Fill in the form:
- Title:
Learn Loopar - Description:
Complete all tutorials - Status:
In Progress - Due Date: (pick a date)
- Priority:
High
- Title:
- Click Save
View in List
- Go back to the list
- Your task appears with all fields
- Try the search and filters!
Step 5: Explore the Generated Features
Automatic Features
| Feature | How to Access |
|---|---|
| Search | Type in the search box |
| Filters | Click filter icon, select status |
| Pagination | Bottom of the list |
| Edit | Click on any task |
| Delete | Select 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
- Go to Core ā Page
- Click + New
- Fill in:
| Field | Value |
|---|---|
| Name | task-manager-home |
| Module | task-manager |
| Title | Task Manager - Get Things Done |
Step 2: Build the Hero Section
Add a Section
- From components, drag Section into the page
- This is your full-width container
Add a Row and Columns
- Inside the section, drag a Row
- Configure layout:
[50, 50](two equal columns)
Left Column - Text Content
-
Click on the left column
-
Drag a Title into it:
- Text:
Manage Tasks Like a Pro - Class:
display-4 mb-4
- Text:
-
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
- Text:
-
Drag a Link (button):
- Label:
Get Started Free - To:
/desk/Task/list - Variant:
default
- Label:
Right Column - Image
- Click on the right column
- 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
- Drag a new Section below the hero
- Add a Text Block centered:
- Subtitle:
Features - Title:
Everything you need to stay productive
- Subtitle:
Add Feature Cards
- Drag a Row with layout
[33, 33, 33] - 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
-
Drag a new Section
-
Add a Panel with:
- Variant:
gradient - Class:
text-center p-12
- Variant:
-
Inside the panel:
- Title:
Ready to get organized? - Paragraph:
Start managing your tasks today. It's free. - Link:
Start Nowā/desk/Task/new
- Title:
Step 5: Save and Preview
- Click Save
- Open a new tab:
http://localhost:3000/task-manager-home - See your landing page live!
Step 6: Add Animations (Optional)
Make it fancy:
- Select any element
- In properties, set:
- Animation:
fade-up,fade-right,zoom-in - Animation Delay:
100,200,300(stagger effect)
- Animation:
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
| File | Purpose | URL Accessible |
|---|---|---|
task.js | Business logic, hooks, data methods | ā No |
task-controller.js | API endpoints | ā
Yes (only action* methods) |
task.jsx | React 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
- Restart your dev server (or wait for hot reload)
- Try creating a task with a 2-character title ā Error!
- Try setting a past due date ā Error!
- Create a task, then set status to Completed ā
completed_dateauto-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
- Go to Task Manager ā Task
- Click on any task or create a new one
- 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
| Need | File | URL Accessible |
|---|---|---|
| Validation rules | Model .js | ā No |
| Helper methods | Model .js | ā No |
| Lifecycle hooks | Model .js | ā No |
| API endpoints | Controller -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
- Go to Core ā Entity ā + New
- Configure:
| Field | Value |
|---|---|
| Name | Project |
| Module | task-manager |
-
Add fields:
- name (Data, Required)
- description (Text)
- status (Select:
Active,On Hold,Completed) - start_date (Date)
- end_date (Date)
-
Save the entity
Step 2: Link Task to Project
-
Open the Task entity for editing
-
Add a new Link field:
- Name:
project - Label:
Project - Options:
Project
- Name:
-
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 documentloopar.db.getAll()queries multiple documentsloopar.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
- Go to Core ā Form ā + New
- Configure:
| Field | Value |
|---|---|
| Name | contact-form |
| Module | task-manager |
| Title | Contact Us |
-
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")
-
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
| Pattern | Model (.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
- Create a new tenant for a "client"
- Install our Task Manager app
- Configure a domain
- Deploy to production with SSL
Step 1: Create a New Tenant
- In your dev site, go to Core ā Tenant
- Click + New
- Configure:
| Field | Value |
|---|---|
| Name | acme-corp |
| Port | 3001 |
| Database Type | MySQL (or SQLite for testing) |
| Database Name | acme_corp_db |
- Click Save
Loopar creates:
- ā
New directory:
sites/acme-corp/ - ā
Environment file:
sites/acme-corp/.env - ā PM2 process configuration
Step 2: Start the Tenant
- In the tenant list, find
acme-corp - Click Start
- 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
- Open
http://localhost:3001 - Complete the setup wizard:
- Database configuration
- Admin account
- Project info
- Log in to the tenant's Desk
Step 4: Install Your App
- In the tenant's Desk, go to Core ā App Manager
- Find
task-managerin the available apps - 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
- Go back to your dev site
- Open Core ā Tenant ā
acme-corp - Set the domain:
| Field | Value |
|---|---|
| Domain | tasks.acme-corp.com |
| Production | ā Yes |
- Click Save
Step 6: Deploy to Production
- In the tenant record, click Set on Production (or Deploy)
- 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
- Explore the Components Reference
- Check the API Reference
- Join our GitHub community
Happy building! š