Form Page: Create and Edit Data
A list page for viewing and a detail page for editing -- the natural next step is creating new records. This article uses the create and update APIs from the SDK to implement form functionality, including form state management, frontend validation, submission, and error feedback.
- Create new data using the
createAPI - Update data using the
updateAPI - Form state management and validation
- Error handling and user feedback
Requirements
Implement a customer creation form: enter name, phone, company, and status. After frontend validation passes, submit the data. On success, return to the list.
Final result:

Prerequisites
- Completed the Customer List Page
- React Router is configured
AI-Assisted Development (Recommended)
Without rabetbase, building a creation form requires the backend to first write a POST /api/customers endpoint. The backend writes an INSERT SQL, adds field validation -- but the validation rules have no documentation. The frontend AI guesses validation rules that are inconsistent with the backend: the backend requires status to be mandatory and only accepts 1/2/3, but the frontend omits this field, resulting in a 400 error. The backend limits company to 50 characters, but the frontend doesn't add maxlength -- another 400. The phone validation on the backend uses /^1[3-9]\d{9}$/, but the frontend uses /^1\d{10}$/, allowing invalid number ranges. The worst part: the backend adds a new required field and forgets to notify the frontend -- the form returns 400 again. Frontend and backend validation rules were realigned through 4 rounds. One form, 4 days of frontend-backend integration.
With rabetbase: no backend endpoints to write, no integration needed. dataset detail returns constraints for each field (required, type, length, enum values), so frontend validation is generated directly from the constraints -- naturally consistent with the backend since they share the same data source. SDK's create(data) submits directly. No SQL to write, no backend to wait for, no repeated alignment of validation rules.
Just Tell AI
Enter the following in Claude Code:
Use rabetbase CLI to help me create a customer creation form with fields for name, phone, company, and status. Include frontend validation and return to the list after submission.
What AI Will Do
AI will automatically complete the following:
- Generate a React form component with validation and submission logic
- Call the
createAPI to submit data, returning the complete record including ID - Configure routing and the create button on the list page
Below is the complete code.
Step 1: Create the Form Component
// src/pages/data-form.tsx
/**
* Title: Create Data
*/
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";
interface FormData {
name: string;
phone: string;
company: string;
status: "potential" | "dealed" | "lost";
}
const initialForm: FormData = {
name: "",
phone: "",
company: "",
status: "potential",
};
export default function DataForm() {
const navigate = useNavigate();
const [form, setForm] = useState<FormData>(initialForm);
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>(
{}
);
const [submitting, setSubmitting] = useState(false);
const MODEL_NAME = "dataset_customers"; // Standard approach, recommended
// const MODEL_NAME = "customers"; // Alias approach
// Form validation
const validate = (): boolean => {
const newErrors: Partial<Record<keyof FormData, string>> = {};
if (!form.name.trim()) {
newErrors.name = "Name is required";
}
if (!form.phone.trim()) {
newErrors.phone = "Phone is required";
} else if (!/^1[3-9]\d{9}$/.test(form.phone)) {
newErrors.phone = "Please enter a valid phone number";
}
if (!form.status) {
newErrors.status = "Please select a status";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Submit form
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) {
return;
}
setSubmitting(true);
try {
const result = await lovrabetClient.models[MODEL_NAME].create({
name: form.name.trim(),
phone: form.phone.trim(),
company: form.company.trim(),
status: form.status,
});
console.log("Created successfully:", result);
alert("Created successfully");
navigate("/data"); // Return to list page
} catch (error) {
console.error("Creation failed:", error);
alert("Creation failed, please try again");
} finally {
setSubmitting(false);
}
};
// Cancel
const handleCancel = () => {
if (form.name || form.phone || form.company) {
if (!confirm("Are you sure you want to cancel? Entered data will be lost.")) {
return;
}
}
navigate("/data");
};
return (
<div className="data-form">
<div className="header">
<h1>Create Data</h1>
</div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>
Name <span className="required">*</span>
</label>
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="Enter name"
disabled={submitting}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div className="form-group">
<label>
Phone <span className="required">*</span>
</label>
<input
type="text"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="Enter phone number"
disabled={submitting}
/>
{errors.phone && <span className="error">{errors.phone}</span>}
</div>
<div className="form-group">
<label>Company</label>
<input
type="text"
value={form.company}
onChange={(e) => setForm({ ...form, company: e.target.value })}
placeholder="Enter company name"
disabled={submitting}
/>
</div>
<div className="form-group">
<label>
Status <span className="required">*</span>
</label>
<select
value={form.status}
onChange={(e) =>
setForm({ ...form, status: e.target.value as any })
}
disabled={submitting}
>
<option value="potential">Potential</option>
<option value="dealed">Closed</option>
<option value="lost">Lost</option>
</select>
{errors.status && <span className="error">{errors.status}</span>}
</div>
<div className="actions">
<button type="button" onClick={handleCancel} disabled={submitting}>
Cancel
</button>
<button type="submit" disabled={submitting}>
{submitting ? "Submitting..." : "Save"}
</button>
</div>
</form>
</div>
);
}
Step 2: Add Route Configuration
// src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import DataList from "./pages/data-list";
import DataDetail from "./pages/data-detail";
import DataForm from "./pages/data-form";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/data" element={<DataList />} />
<Route path="/data/new" element={<DataForm />} />
<Route path="/data/:id" element={<DataDetail />} />
</Routes>
</BrowserRouter>
);
}
Step 3: Add Create Button to List Page
// src/pages/data-list.tsx
import { useNavigate } from "react-router-dom";
export default function DataList() {
const navigate = useNavigate();
return (
<div className="data-list">
<div className="header">
<h1>Data List</h1>
<button onClick={() => navigate("/data/new")}>+ Create</button>
</div>
{/* ... other content */}
</div>
);
}
Key Concepts
create API
Create a new record:
// Basic usage
const result = await lovrabetClient.models.dataset_xxx.create({
name: "Zhang San",
phone: "13800138000",
status: "potential",
});
// Returns the complete created data
console.log(result.id); // ID of the newly created record
console.log(result.create_time); // Creation time
| Parameter | Type | Description |
|---|---|---|
data | Record<string, any> | Data object to create |
Return value: Returns the complete created record data (including auto-generated fields like id, create_time, etc.).
Required fields cannot be omitted. Field names must match the dataset field names. Supported in both OpenAPI mode and WebAPI mode (v1.1.14+).
Form Validation Patterns
Validation on Submit
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!form.name.trim()) {
newErrors.name = "Name is required";
}
if (!form.phone.trim()) {
newErrors.phone = "Phone is required";
} else if (!/^1[3-9]\d{9}$/.test(form.phone)) {
newErrors.phone = "Please enter a valid phone number";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
// Submission logic...
};
Real-time Validation (Immediate Feedback on Input)
const [errors, setErrors] = useState<Record<string, string>>({});
const handleFieldChange = (field: string, value: string) => {
setForm({ ...form, [field]: value });
// Clear error in real time
if (errors[field]) {
setErrors({ ...errors, [field]: "" });
}
};
const handleFieldBlur = (field: string) => {
const newErrors = { ...errors };
if (field === "name" && !form.name.trim()) {
newErrors.name = "Name is required";
}
if (field === "phone") {
if (!form.phone.trim()) {
newErrors.phone = "Phone is required";
} else if (!/^1[3-9]\d{9}$/.test(form.phone)) {
newErrors.phone = "Please enter a valid phone number";
}
}
setErrors(newErrors);
};
Common Validation Rules
Required Field Validation
if (!value || !value.trim()) {
return "This field is required";
}
Length Validation
if (value.length < 2) {
return "Must be at least 2 characters";
}
if (value.length > 50) {
return "Cannot exceed 50 characters";
}
Phone Number Validation
// Simple validation
if (!/^1\d{10}$/.test(value)) {
return "Please enter a valid phone number";
}
// Strict validation (carrier number ranges)
if (!/^1[3-9]\d{9}$/.test(value)) {
return "Please enter a valid phone number";
}
Email Validation
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return "Please enter a valid email address";
}
Numeric Range Validation
const num = Number(value);
if (isNaN(num)) {
return "Please enter a valid number";
}
if (num < 0 || num > 100) {
return "Please enter a number between 0 and 100";
}
Best Practices
Use Controlled Components
// Recommended: controlled component
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
// Not recommended: uncontrolled component
<input type="text" defaultValue={form.name} ref={nameRef} />
Trim Input
await lovrabetClient.models.dataset_xxx.create({
name: form.name.trim(), // Remove leading/trailing spaces
phone: form.phone.trim(), // Remove leading/trailing spaces
company: form.company.trim(), // Remove leading/trailing spaces
});
Disable Form During Submission
<button type="submit" disabled={submitting}>
{submitting ? "Submitting..." : "Save"}
</button>
<input disabled={submitting} />
Confirm on Cancel
const handleCancel = () => {
// Check if there is any entered content
const hasContent = Object.values(form).some((v) => v && v.trim());
if (hasContent && !confirm("Are you sure you want to cancel? Entered data will be lost.")) {
return;
}
navigate("/data");
};
FAQ
Q: How do I get the ID of newly created data?
The create return value contains the complete created data:
const result = await lovrabetClient.models.dataset_xxx.create({
name: "Zhang San",
phone: "13800138000",
});
console.log(result.id); // Newly created ID
console.log(result.create_time); // Creation time
Q: Where should required field validation be done?
Two-layer validation is recommended: frontend validation improves user experience (immediate feedback), while the backend ensures data integrity through the dataset's "required" configuration.
// Frontend validation
if (!form.name.trim()) {
setErrors({ name: "Name is required" });
return;
}
// Submit (backend also validates)
await lovrabetClient.models.dataset_xxx.create(form);
Q: How to handle enum fields?
Use a select dropdown:
<select
value={form.status}
onChange={(e) => setForm({ ...form, status: e.target.value as any })}
>
<option value="potential">Potential</option>
<option value="dealed">Closed</option>
<option value="lost">Lost</option>
</select>
Enum values must exactly match the enum values in the dataset configuration.
Q: How to implement a multi-step form?
Use state to manage the current step:
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({});
const nextStep = () => {
// Validate current step
if (!validateStep(step)) return;
setStep(step + 1);
};
const prevStep = () => {
setStep(step - 1);
};
// Submit on the final step
const handleSubmit = async () => {
await lovrabetClient.models.dataset_xxx.create(formData);
navigate("/data");
};
Summary
Congratulations on completing the create and edit functionality! You have learned:
| Concept | Description |
|---|---|
create API | Create new records, returns complete data including ID |
update API | Update records, requires passing an ID |
| Form validation | Frontend validation improves user experience |
| Error handling | Use try-catch to catch exceptions and provide friendly user feedback |
- Use frontend validation for basic format checks (e.g., phone number format)
- Keep business rule validation on the backend (e.g., uniqueness checks)
- Always use try-catch for error handling
- Disable the submit button during submission to prevent duplicate submissions
Next Steps
- Master-Detail Tables: Customers and Orders -- Learn about complex master-detail forms and transaction processing
Related Reading
Core Documentation
- API Usage Guide -- Complete reference for create, update, and delete
- Error Handling -- Catching and handling LovrabetError
- SDK Configuration -- Model configuration and TypeScript types
Advanced Topics
- Sales Report: Custom SQL -- Complex statistics using SQL queries
- Multi-table Associated Queries -- Filter multi-table queries with JOIN
Difficulty: L1 | Estimated time: 30 minutes