Schema and Validation
Use Frontbacked.state.check(path, schema, check?) to validate browser state before calling APIs such as signUp, createPost, updatePost, or updateUserData.
Client validation is for fast feedback: it catches missing values, wrong file types, oversized files, and invalid form shapes before the user waits on a request. FRL schemas still protect saved data, so use both: FQL validation for friendly forms, FRL validation for the final data contract.
Reuse FRL Schemas
When a theme has FRL rules, use Frontbacked.schemas.post(...) or Frontbacked.schemas.profile(...) to start from that saved-data contract and adjust only what the browser form needs.
const productFormSchema = Frontbacked.schemas.post("products", {
action: "create",
omit: ["status"],
add: {
agreeToTerms: {
type: "boolean",
accepted: true,
messages: { accepted: "Accept the terms to publish this product." }
}
},
feedback: { to: "formErrors" }
});
const result = Frontbacked.state.check("productForm", productFormSchema);
Use pick for small forms, omit for fields the page should not submit, patch for field-specific browser messages, and add for browser-only checks such as terms acceptance.
Basic Form Validation
<!-- {STATE}
state = {
"signup": {
"name": "#signupName.oninput.target.value",
"email": "#signupEmail.oninput.target.value",
"password": "#signupPassword.oninput.target.value",
"confirmPassword": "#signupConfirmPassword.oninput.target.value",
"terms(false)": "#signupTerms.onchange.target.checked"
},
"signupStatus": "#signupForm.onsubmit.handleSignup()"
}
-->
<form id="signupForm">
<input id="signupName" placeholder="Name">
<input id="signupEmail" placeholder="Email">
<input id="signupPassword" type="password" placeholder="Password">
<input id="signupConfirmPassword" type="password" placeholder="Confirm password">
<label><input id="signupTerms" type="checkbox"> I accept the terms</label>
<button>Create account</button>
</form>
const signupSchema = {
name: { type: "string", required: true, trim: true, min: 2, max: 80 },
email: { type: "email", required: true, trim: true },
password: { type: "string", required: true, min: 8, max: 128 },
confirmPassword: {
type: "string",
required: true,
sameAs: "signup.password",
messages: { sameAs: "Passwords must match." }
},
terms: {
type: "boolean",
accepted: true,
messages: { accepted: "Please accept the terms to continue." }
}
};
async function handleSignup() {
const result = Frontbacked.state.check("signup", signupSchema);
if (!result.ok) return null;
return await Frontbacked.signUp({
email: result.values.email,
password: result.values.password,
name: result.values.name
});
}
Frontbacked.functions.define({ handleSignup });
result.values contains normalized values. In the example above, name is trimmed and email is trimmed and lowercased.
Validate Post Data Before Upload
File rules work alongside normal string, number, object, and array rules.
<!-- {STATE}
state = {
"articleForm": {
"title": "#articleTitle.oninput.target.value",
"category('market')": "#articleCategory.onchange.target.value",
"body": "#articleBody.oninput.target.value",
"cover": "#articleCover.onchange.target.files[0]",
"video": "#articleVideo.onchange.target.files[0]",
"tags": []
},
"articlePublishStatus": "#publishArticleForm.onsubmit.publishArticle()"
}
-->
<form id="publishArticleForm">
<input id="articleTitle">
<select id="articleCategory">
<option value="market">Market</option>
<option value="education">Education</option>
<option value="news">News</option>
</select>
<textarea id="articleBody"></textarea>
<input id="articleCover" type="file" accept="image/jpeg,image/png,image/webp">
<input id="articleVideo" type="file" accept="video/mp4,video/webm">
<button>Publish</button>
</form>
const articleSchema = {
title: { type: "string", required: true, trim: true, min: 3, max: 140 },
category: { type: "enum", enums: ["market", "education", "news"], required: true },
body: { type: "string", required: true, min: 20, max: 50000 },
cover: {
type: "file",
required: true,
maxSize: 7000000,
mimeTypes: ["image/jpeg", "image/png", "image/webp"]
},
video: {
type: "file",
maxSize: 500000000,
maxDurationSeconds: 300,
mimeTypes: ["video/mp4", "video/webm", "video/quicktime"]
},
tags: {
type: "array",
max: 10,
of: { type: "string", trim: true, min: 1, max: 40 }
}
};
async function publishArticle() {
const result = Frontbacked.state.check("articleForm", articleSchema);
if (!result.ok) return null;
return await Frontbacked.createPost({
type: "articles",
post: result.values,
onUploadProgress(progress) {
Frontbacked.state.set(`uploads.${progress.sessionId}`, progress);
}
});
}
Frontbacked.functions.define({ publishArticle });
maxSize and mimeTypes can be checked immediately from the selected file. maxDurationSeconds keeps your client schema aligned with the FRL video playback cap. If an uploaded video is longer than the allowed duration, Frontbacked prepares playback from the beginning up to that limit.
Nested Objects
Use fields for nested objects:
const profileSchema = {
displayName: { type: "string", required: true, trim: true, max: 80 },
location: {
type: "object",
fields: {
city: { type: "string", trim: true, max: 80 },
country: { type: "string", trim: true, max: 80 }
}
},
socials: {
type: "object",
fields: {
website: { type: "string", trim: true, max: 200 },
x: { type: "string", trim: true, max: 80 }
}
}
};
const result = Frontbacked.state.check("profile", profileSchema);
if (result.ok) {
await Frontbacked.updateUserData({
data: result.values,
merge: true
});
}
Validate One Field
Use the optional check object when validating several fields separately, such as on blur:
const check = { hasError: false, errors: [] };
Frontbacked.state.check("signup.email", signupSchema.email, check);
Frontbacked.state.check("signup.password", signupSchema.password, check);
if (check.hasError) {
console.log(check.errors);
}
Custom Rules
Use validate when a field needs theme-specific logic:
const schema = {
coupon: {
type: "string",
trim: true,
validate(value) {
if (!value) return true;
return /^SAVE-[A-Z0-9]{6}$/.test(value) || "Coupon codes look like SAVE-ABC123.";
}
}
};
Feedback Options
By default, Frontbacked places messages near matching inputs when it can. You can override where a message goes:
const schema = {
avatar: {
type: "file",
maxSize: 3000000,
mimeTypes: ["image/jpeg", "image/png"],
feedback: { to: "toast", duration: 4000 },
messages: {
maxSize: "Use an image smaller than 3MB.",
mimeTypes: "Use a JPEG or PNG image."
}
}
};
Useful feedback.to values:
| Value | Behavior |
|---|---|
"alert" | Show a Frontbacked alert. |
"toast" | Show a temporary toast. |
"formErrors" | Write messages to the form error area. |
"customFunction()" | Call a global function with the message payload. |
Schema Keys
| Key | Purpose |
|---|---|
type | string, email, number, boolean, object, array, file, datetime, or post. |
required | Require a present value. Empty strings, empty arrays, and missing files fail. |
trim, lowercase, uppercase | Normalize string values before validation returns. email lowercases by default. |
min, max | String length, number value, or array length limit. |
pattern | Regular expression for string values. |
enum | Allowed values. |
sameAs | Require the value to equal another state path. |
accepted | Require a boolean value to be true, useful for terms checkboxes. |
maxSize | Maximum file size in bytes. |
mimeTypes | Allowed file MIME types. |
maxDurationSeconds | Maximum prepared video playback duration in seconds. |
fields | Nested object schema. |
of or items | Array item schema. |
messages | Optional custom messages by failed rule, such as required, email, min, or max. |
feedback | Message destination options. |
validate | Custom validation function that receives (value, state). |
allowFalse | Treat boolean false as valid. |
allowEmptyString | Treat an empty string as valid. |
allowEmptyArray | Treat an empty array as valid. |