Backend Function (I): Pre-validation Hook - Data Validation and Interception
Backend Function is one of the core capabilities of the Lovrabet platform, allowing you to execute custom business logic on the backend. Starting from this section, we will systematically learn the Backend Function system through three articles:
- This Article (I): Pre-validation Hook -- Automatic validation and interception before data operations
- Next Article (II): Post-validation Hook -- Automatic processing and transformation after data operations
- Next Article (III): Standalone Endpoint -- Custom API endpoints and complex SQL calls
Why Do You Need Pre-validation Hooks?
Previous sections implemented list, detail, and create features, with basic validation added on the frontend. However, some validations must be done on the backend -- such as phone number uniqueness, complex business rules, and data access control, which cannot be bypassed through the frontend.
This section introduces Pre-validation Hooks (Before Scripts): automatically triggered before data operations execute. If validation fails, the operation is blocked; if it passes, data is written to the database.
- What is a Pre-validation Hook (Before Script)
- How to configure Pre-validation Hooks to intercept data operations
- Common validation scenario implementations
- Differences between Pre-validation Hooks and frontend validation
Requirements
Before data is written to the database, automatically execute business rule validation:
- Prevent duplicate data (phone number, email uniqueness)
- Business rule validation (e.g., age restrictions)
- Data access control (e.g., only operate on data within your department)
- Auto-populate data (e.g., set department affiliation)
The overall workflow is:
User submits form
|
Frontend validation (basic format)
|
Pre-validation Hook (business rule validation)
|
Validation passed -> Write to database
Validation failed -> Return error message
Core Concepts
Pre-validation Hooks are also called Before Scripts or HOOK pre-scripts, automatically triggered before data operations execute. Throwing an exception in the function will block the operation, making it suitable for data validation, permission control, and data population.
| Feature | Description |
|---|---|
| Trigger timing | Before create, update, delete, filter operations |
| Execution location | Backend (Lovrabet server environment) |
| Blocking capability | Throwing an exception blocks the operation |
| Function naming | beforeCreate, beforeUpdate, beforeFilter, etc. |
Implementation Steps
AI-Assisted Development (Recommended)
Without rabetbase, backend validation means you must be able to modify the backend code. But the API is platform-provided, you can't change it. Your options: 1) Build a proxy server to intercept all requests and add validation -- essentially building a middleware layer, with authentication forwarding, request forwarding, error handling all to write yourself, another 3 days; 2) Submit a ticket to the platform team to add validation -- scheduled for next sprint, you wait two weeks; 3) Rely only on frontend validation -- someone uses Postman to bypass the frontend and call the API directly, duplicate data gets in. If you choose option 1, building the proxy server takes another 3 days, and performance degrades after launch -- all requests go through an extra layer. A single "phone number must be unique" requirement becomes an infrastructure project. With rabetbase: no backend code to modify, no proxy server to build. The BaaS HOOK system automatically intercepts on the backend -- frontend forms, API calls, Postman, nothing can bypass it. The CLI's bff new --type HOOK creates a beforeCreate scaffold (parameter structure ready), and Skills guide AI to write validation logic. bff push deploys and takes effect. Not a single line of frontend code changes, zero performance impact.
All You Need to Tell AI
In Claude Code, enter:
Use rabetbase CLI to help me create a Pre-validation Hook (HOOK Before) for the create operation on the customer dataset, validating phone number and email uniqueness, age must be at least 18, and auto-populating the department.
What AI Will Do
AI will use rabetbase CLI to automatically complete the following:
- Query the customer dataset structure (
rabetbase dataset detail) - Create a HOOK scaffold (
rabetbase bff new --type HOOK) - Write complete validation logic (phone/email uniqueness, age restriction, department population)
- Push to the platform to take effect (
rabetbase bff push)
Once complete, the Pre-validation Hook takes effect immediately, with no frontend code changes required. Below is the complete code generated by AI, for your understanding and future maintenance.
Manual Operation (Alternative)
Method 1: Manual CLI
# 1. Create scaffold
rabetbase bff new --type HOOK --name beforeCreate --function-node before --alias customers --operation-type create
# 2. Open the generated file in editor
code .rabetbase/bff/<appCode>/HOOK/customers/create/before/beforeCreate.js
# 3. Push
rabetbase bff push --yes --type HOOK --name beforeCreate
Method 2: Platform UI
- Open the dataset's API list page:
https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list - Select the operation to configure (e.g.,
create) - In the "Business Logic Extension" section, find the "Pre-validation Hook" card
- Click the "Write" button, paste the code and save
Step 1: Complete Code Reference
Whether using AI or manual operation, the Pre-validation Hook code created is as follows:
/**
* Customer creation Pre-validation Hook - Data uniqueness validation and business rule verification
*
* [API Path] POST /api/{appCode}/customers/create
* Rule: /api/{applicationCode}/{datasetCode}/create
*
* [Platform Configuration] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* Note: appCode is the application code, datasetId is the dataset ID (integer, obtained from MCP get_dataset_detail)
*
* [HTTP Request Body Parameters]
* { "name": "Customer Name", "phone": "Phone Number", "email": "Email" }
*
* [Return Data Structure]
* HOOK: Returns modified params object
*
* @param {Object} params - Request parameters
* @param {Object} context - Execution context (automatically injected by platform, no need to pass when calling)
* @param {Object} context.userInfo - Current user information
* @param {Object} context.client - Database operation entry point
* @returns {Object} Returns modified params
*/
export default async function beforeCreate(params, context) {
// Dataset code mapping table
// Comment format: Dataset: {dataset_name} | Table: {table_name}
const TABLES = {
customers: "dataset_XXXXXXXXXX", // Dataset: Customers | Table: customers (replace with actual 32-character code)
};
const models = context.client.models;
console.log("[Pre-validation Hook] Starting customer creation validation:", params);
// ========== Validation 1: Phone Number Uniqueness ==========
if (params.phone) {
// Dataset: Customers | Table: customers
const existingPhone = await models[TABLES.customers].filter({
where: {
phone: { $eq: params.phone },
...(params.id ? { id: { $ne: params.id } } : {}), // Exclude current record (for updates)
},
});
if (existingPhone.tableData && existingPhone.tableData.length > 0) {
throw new Error("Phone number is already in use, please use a different number");
}
}
// ========== Validation 2: Email Uniqueness ==========
if (params.email) {
// Dataset: Customers | Table: customers
const existingEmail = await models[TABLES.customers].filter({
where: {
email: { $eq: params.email },
...(params.id ? { id: { $ne: params.id } } : {}),
},
});
if (existingEmail.tableData && existingEmail.tableData.length > 0) {
throw new Error("Email is already in use, please use a different email");
}
}
// ========== Validation 3: Age Must Be At Least 18 ==========
if (params.birthday) {
const today = new Date();
const birthDate = new Date(params.birthday);
const age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < birthDate.getDate())
) {
age--;
}
if (age < 18) {
throw new Error("Customer must be at least 18 years old");
}
}
// ========== Auto-populate: Set Department ==========
if (!params.department && context.userInfo?.department) {
params.department = context.userInfo.department;
}
// System fields (create_by, create_time, etc.) are automatically maintained by the platform, no manual setting required
return params;
}
Key Changes:
- Use
models[TABLES.customers]to access the dataset context.userInforeplacescontext.currentUserfilterreturns{ tableData, paging, tableColumns }structure- System fields (create_by, create_time) are automatically maintained by the platform
Step 2: Push to Platform
After writing the code, push it to the platform to take effect. If using AI-assisted development, this step is already done automatically.
Manual push:
# Preview changes first
rabetbase bff status --format json
# Dry-run to confirm push content
rabetbase bff push --type HOOK --name beforeCreate --dry-run --format json
# Official push
rabetbase bff push --yes --type HOOK --name beforeCreate
You can also edit via the platform UI: https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list, click "Write" in the "Pre-validation Hook" card.
Step 3: No Frontend Code Changes Required
Frontend code remains unchanged; the SDK will automatically trigger the Pre-validation Hook:
// Frontend submits form
const result = await lovrabetClient.models.dataset_customers.create({
name: "Zhang San",
phone: "13800138000",
email: "zhangsan@example.com",
birthday: "1990-01-01",
});
// If Pre-validation Hook validation fails, an exception will be thrown
try {
const result = await lovrabetClient.models.dataset_customers.create(data);
console.log("Created successfully:", result);
} catch (error) {
console.error("Creation failed:", error.message);
// Output: Phone number is already in use, please use a different number
}
Advanced Scenarios
The following scenarios can also be completed with AI assistance -- describe your requirements in Claude Code and AI will automatically create the corresponding HOOK and push it. Code reference below:
Multi-Tenant Data Isolation
/**
* Multi-tenant data isolation Pre-validation Hook (Before Create)
* Ensures users can only operate on data within their own tenant
*
* [API Path] POST /api/{appCode}/customers/create
* Rule: /api/{applicationCode}/{datasetCode}/create
*
* [Platform Configuration] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* Note: appCode is the application code, datasetId is the dataset ID (integer, obtained from MCP get_dataset_detail)
*
* [HTTP Request Body Parameters]
* { "name": "Customer Name", ... }
*
* [Return Data Structure]
* HOOK: Returns modified params object
*
* @param {Object} params - Request parameters
* @param {Object} context - Execution context (automatically injected by platform, no need to pass when calling)
* @param {Object} context.userInfo - Current user information
* @returns {Object} Returns modified params
*/
export default async function beforeCreate(params, context) {
const TABLES = {
customers: "dataset_XXXXXXXXXX", // Dataset: Customers | Table: customers (replace with actual 32-character code)
};
const models = context.client.models;
// Get user's tenant (from userInfo)
const userTenantId = context.userInfo?.tenantCode;
if (!userTenantId) {
throw new Error("User is not assigned to a tenant, cannot perform operation");
}
// Create operation: Automatically set tenant ID
params.tenant_id = userTenantId;
return params;
}
Data Status Transition Validation
/**
* Customer status transition validation (Before Update)
* Restricts status changes to follow specified workflow
*
* [API Path] POST /api/{appCode}/customers/update
* Rule: /api/{applicationCode}/{datasetCode}/update
*
* [Platform Configuration] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* Note: appCode is the application code, datasetId is the dataset ID (integer, obtained from MCP get_dataset_detail)
*
* [HTTP Request Body Parameters]
* { "id": 123, "status": "active" }
*
* [Return Data Structure]
* HOOK: Returns modified params object
*
* @param {Object} params - Request parameters
* @param {string} params.id - Record ID
* @param {string} params.status - New status
* @param {Object} context - Execution context (automatically injected by platform, no need to pass when calling)
* @returns {Object} Returns modified params
*/
export default async function beforeUpdate(params, context) {
const TABLES = {
customers: "dataset_XXXXXXXXXX", // Dataset: Customers | Table: customers (replace with actual 32-character code)
};
const models = context.client.models;
if (!params.status || !params.id) {
return params;
}
// Get current record
// Dataset: Customers | Table: customers
const current = await models[TABLES.customers].findOne({ id: params.id });
if (!current) {
throw new Error("Record does not exist");
}
const currentStatus = current.status;
const newStatus = params.status;
// Define allowed status transitions
const allowedTransitions = {
potential: ["dealed", "lost"], // Potential -> Closed/Lost
dealed: ["active", "inactive"], // Closed -> Active/Inactive
lost: ["potential"], // Lost -> Potential (reactivation)
active: ["inactive", "completed"], // Active -> Inactive/Completed
inactive: ["active"], // Inactive -> Active
};
const allowed = allowedTransitions[currentStatus] || [];
if (!allowed.includes(newStatus)) {
throw new Error(`Status change from "${currentStatus}" to "${newStatus}" is not allowed`);
}
return params;
}
function getStatusName(status) {
const names = {
potential: "Potential",
dealed: "Closed",
lost: "Lost",
active: "Active",
inactive: "Inactive",
completed: "Completed",
};
return names[status] || status;
}
Dynamically Fetch External Data for Validation
/**
* Validate customer credit limit
* Fetch customer credit information via external API
*/
export default async function validateCreditLimit(params, context) {
const { data } = params;
if (!data.customerId) {
return { data };
}
// Call external service to query customer credit
const creditInfo = await fetchCreditInfo(data.customerId);
if (creditInfo.blacklisted) {
throw new Error("This customer is blacklisted and cannot create orders");
}
const orderAmount = data.totalAmount || 0;
const availableCredit = creditInfo.creditLimit - creditInfo.usedCredit;
if (orderAmount > availableCredit) {
throw new Error(
`Exceeds credit limit. Available credit: ¥${availableCredit}, Order amount: ¥${orderAmount}`
);
}
return { data };
}
/**
* Simulate calling external credit service
* Replace with actual API call in production
*/
async function fetchCreditInfo(customerId) {
// This could be an HTTP request or other method to call external services
// For example: fetch('https://api.credit-service.com/check/' + customerId)
return {
blacklisted: false,
creditLimit: 100000,
usedCredit: 20000,
};
}
Key Concepts Summary
Before Script Function Signature
export default async function beforeCreate(params, context) {
// params - Request parameters (varies by operation type)
// context - Execution context (automatically injected by platform)
// Database operations
const models = context.client.models;
// Return modified params
return params;
}
params Parameter (varies by operation type)
| Operation Type | params Content |
|---|---|
create | Submitted form data { name: "xxx", phone: "xxx" } |
update | Updated data { id: 123, name: "new" } |
delete | Delete condition { id: 123 } |
filter | Query parameters { where: {...}, select: [...] } |
context Object
| Field | Type | Description |
|---|---|---|
userInfo | Object | Current logged-in user information (id, username, tenantCode) |
client | Object | Database operation entry point, access datasets via models |
appCode | String | Current application code |
Dataset Operation Methods
// Define dataset mapping (at function start)
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 32-character dataset code
};
const models = context.client.models;
// Query single record
const record = await models[TABLES.customers].findOne({ id: 123 });
// Query list
const result = await models[TABLES.customers].filter({
where: { status: { $eq: "active" } },
});
// result.tableData is the data array
// Create
const id = await models[TABLES.customers].create({
name: "Zhang San",
phone: "13800138000",
});
// Update
await models[TABLES.customers].update({
id: 123,
name: "New Name",
});
How to Block Operations
// Throwing an exception will block the operation and return an error message
throw new Error('Error message');
// Normal return continues execution
return params;
FAQ
Q: What's the difference between Pre-validation Hooks and frontend validation?
A: Both should be used, each serving its own purpose:
| Validation Location | Purpose | Advantage |
|---|---|---|
| Frontend Validation | User Experience | Immediate feedback, reduces invalid requests |
| Pre-validation Hook | Data Security | Must execute, cannot be bypassed |
Recommended Practice: Frontend handles basic format validation (e.g., phone number format), Pre-validation Hooks handle business rule validation (e.g., phone number uniqueness).
Q: Can Pre-validation Hooks modify data?
A: Yes. Returning { data: newData } will affect subsequent operations:
export default async function validate(params, context) {
const { data } = params;
// Auto-populate fields
if (!data.createTime) {
data.createTime = new Date().toISOString();
}
// Return modified data
return { data };
}
Q: How to debug Pre-validation Hooks?
A: Use console.log to output logs, then view them via CLI:
rabetbase logs show --format json
Q: Do Pre-validation Hooks affect performance?
A: Pre-validation Hooks are executed for every data operation, so:
- Avoid time-consuming operations: Don't call slow external APIs in Pre-validation Hooks
- Reduce database queries: Combine multiple queries, use indexes
- Use caching: For infrequently changing data (like configuration info), caching can be used
// Not recommended: Multiple queries
const TABLES = {
customers: "dataset_XXXXXXXXXX",
};
const models = context.client.models;
const existing1 = await models[TABLES.customers].filter({
where: { phone: { $eq: "13800138000" } },
});
const existing2 = await models[TABLES.customers].filter({
where: { email: { $eq: "test@example.com" } },
});
// Recommended: Single query
const existing = await models[TABLES.customers].filter({
where: {
$or: [
{ phone: { $eq: "13800138000" } },
{ email: { $eq: "test@example.com" } },
],
},
});
Section Summary
Congratulations on learning Pre-validation Hooks! Key takeaways:
| Concept | Description |
|---|---|
| Before Script | Validation function that executes automatically before data operations |
throw Error() | Throwing an exception blocks operation execution |
context.userInfo | Get current logged-in user information |
models[TABLES.xxx] | Access datasets in BF |
- Frontend validation for format checking, Pre-validation Hooks for business rules
- Avoid executing time-consuming operations in Pre-validation Hooks
- Use batch queries to optimize performance
- Add logs for easier debugging
Next Steps
- Post-Save Data Processing -- Learn about data masking and format conversion
Related Reading
Core Documentation
- BFF API Reference -- Complete Backend Function usage guide
- SDK Configuration -- Environment setup and authentication settings
- Syntax Sugar -- Convenient functions like safe, sqlSafe
Advanced Topics
- Data Masking: Post-validation Hook -- Unified backend data format processing
- Master-Detail Forms: Transaction Processing -- BF standalone endpoints for complex business logic
- Sales Reports: Custom SQL -- Custom SQL queries
Difficulty Level: L2 | Estimated Time: 35 minutes