Every B2C Commerce SFRA site has dozens of forms. Login. Register. Checkout shipping address. Checkout billing address. Account update. Password reset. Contact us. Support request. The SFRA forms framework is opinionated about how all of these should work. Developers who fight the framework end up with inconsistent error messages, broken accessibility, and validation that fires in the wrong order.
Sapota's Salesforce team has shipped forms across retail and B2B SFRA sites. The patterns that work are not always in the documentation but become reflexive after a few sprints.
The SFRA form framework
SFRA forms are defined in XML files (cartridge/forms/default/...), processed server-side by the form binding layer, and rendered client-side via standard HTML form elements. Three things make up a form's behavior:
1. Form definition (XML). Declares the fields, their types, whether they are mandatory, length limits, regex validators.
2. Form action (controller code). Reads submitted values, runs validation, calls business logic, returns either success or rendered form with errors.
3. Form rendering (ISML). Reads the form object from pdict and renders the fields, validation errors, and submit button.
The default flow is server-side first: form submission posts to a controller endpoint, server validates and re-renders the page with errors if needed. Client-side validation is a progressive enhancement; the server is the source of truth.
Form definition patterns
A standard form definition (forms/default/billing.xml):
<?xml version="1.0"?>
<form xmlns="http://www.demandware.com/xml/form/2008-04-19">
<field formid="firstName" type="string" mandatory="true"
label="label.firstname" min-length="2" max-length="50"
missing-error="error.firstname.missing"/>
<field formid="email" type="string" mandatory="true"
label="label.email" regexp="^[^@]+@[^@]+\.[^@]+$"
missing-error="error.email.missing"
parse-error="error.email.invalid"/>
</form>
Two attributes matter most:
missing-error: the error message key when the field is empty.parse-error: the error message key when the field fails type/regex validation.
These reference resource keys (resolvable to localized strings). All error messages are in resource files, not hardcoded in XML.
Server-side validation that holds up
The controller reads the submitted form, runs clearFormElement if rejecting, and re-renders. A typical pattern:
server.post('Handle', function (req, res, next) {
var billingForm = server.forms.getForm('billing');
// Validation hook for custom rules beyond what XML covers.
if (!isPostalCodeValid(billingForm.postal.value, billingForm.country.value)) {
billingForm.postal.valid = false;
billingForm.postal.error = Resource.msg(
'error.postal.invalid', 'forms', null
);
}
if (!billingForm.valid) {
res.render('checkout/billing/billingDetails', {
billingForm: billingForm
});
} else {
// Persist and continue.
saveBillingAddress(billingForm);
res.redirect(URLUtils.url('Checkout-Submit'));
}
next();
});
Two patterns to internalize:
XML for structural validation, code for business rules. XML handles missing, length, regex. Code handles cross-field validation (passwords must match), business validation (postal code valid for country), database checks (email already in use).
Re-render the form on error, redirect on success. The post-redirect-get pattern prevents form re-submission on browser back. Use res.render when the form has errors; use res.redirect when it succeeds.
Error rendering in ISML
The form rendering pattern that produces consistent UX:
<form action="${URLUtils.url('Billing-Handle')}" method="POST">
<isinclude template="components/forms/csrf"/>
<div class="form-group ${pdict.billingForm.firstName.error ? 'has-error' : ''}">
<label for="firstName"><isprint value="${Resource.msg('label.firstname', 'forms', null)}"/></label>
<input type="text" id="firstName" name="${pdict.billingForm.firstName.htmlName}"
value="${pdict.billingForm.firstName.htmlValue}"
class="form-control"
required="${pdict.billingForm.firstName.mandatory}">
<isif condition="${pdict.billingForm.firstName.error}">
<div class="invalid-feedback"><isprint value="${pdict.billingForm.firstName.error}"/></div>
</isif>
</div>
<button type="submit"><isprint value="${Resource.msg('action.submit', 'forms', null)}"/></button>
</form>
Three things to enforce in every form template:
1. CSRF token included. Use the standard components/forms/csrf include. Forms without CSRF are vulnerable to cross-site request forgery.
2. htmlName and htmlValue from the form object. The framework generates the right name attribute and pre-fills the value on re-render. Hand-rolling these breaks the binding.
3. Error display inline with the field. Errors next to the field they reference, not aggregated at the top. Better UX and better accessibility.
Client-side validation
Client-side validation provides immediate feedback. SFRA ships with a jQuery-based client validator that reads the same form definition the server uses. The pattern:
- Native HTML5 validation runs first (the
requiredattribute, basic type checks). - jQuery handles complex validation (regex, custom rules) on blur or before submit.
- On submit, the form posts to the server regardless; the server is the source of truth.
The mistake to avoid: client-side-only validation. A determined user can bypass it via curl or developer tools. Every validation rule must exist server-side; client-side is enhancement.
Sapota's form discipline
Five practices that ship reliable forms:
1. One controller per form action. A Login-Handle for login, Register-Handle for registration. Avoid multi-purpose controllers that branch on form type. Easier to test, easier to maintain.
2. All error messages in resource files. No hardcoded strings in code or templates. Localization happens by adding locale-specific resource files.
3. Server-side validation duplicated in code for cross-field rules. XML handles per-field. Cross-field (passwords match, billing country requires postal) handled in the controller after server.forms.getForm().
4. CSRF on every state-changing form. Login, register, checkout, account update, anywhere data is written. CSRF is not optional.
5. Accessibility from the start. Label-for-input pairing, aria-invalid on error fields, focus management after submit error (focus the first invalid field).
Common form mistakes
Five patterns Sapota has seen on engagements:
1. Validation only on the client. Forms that pass without server validation. Trivially bypassed. Always validate server-side.
2. Errors aggregated at the top of the page. A user types a 50-character email, gets to the bottom of the form, hits submit, sees "Errors above" but cannot find which field. Inline errors next to the field.
3. Resubmission on back navigation. Form posts directly to a non-redirect controller. Browser back replays the post. Charges customers twice or creates duplicate records. Use post-redirect-get.
4. Hardcoded error messages. A form that bypasses the resource framework and renders hardcoded English error text. Localization broken. Move to Resource.msg().
5. CSRF tokens missing or static. A form without CSRF or with a static token. Vulnerable to attack. Use the framework's CSRF include.
What good form engineering looks like
A B2C Commerce site with healthy form discipline:
- Form definitions in XML, validation rules colocated with field definitions.
- Server-side validation comprehensive; client-side enhancement only.
- Inline error messages next to their fields.
- Post-redirect-get on every state-changing form.
- CSRF tokens on every form.
- Resource files for all user-facing strings.
- Accessibility verified (labels, aria-invalid, focus management).
Forms are the most-interacted-with components on a B2C site. The framework is sufficient when used as intended. Sapota's Salesforce team holds the line on these patterns during code review because forms with subtle bugs produce customer service tickets for months.
Auditing or building forms in a B2C Commerce SFRA site? Sapota's Salesforce team, certified on B2C Commerce Developer (Comm-Dev-101), handles form architecture, validation strategy, and accessibility on production engagements. Get in touch ->
See our full platform services for the stack we cover.








