How to perform server-side form validations using Bootstrap 5, plain JavaScript and Marketing Cloud API

How to perform server-side form validations using Bootstrap 5, plain JavaScript and Marketing Cloud API

This article explains how to perform server-side form validations using Bootstrap 5, plain JavaScript (AJAX) and Marketing Cloud API (SSJS + REST).

Use case

Setting up server-side form validations is not an easy feat, especially in Marketing Cloud.

But when implemented the right way, they can make a huge impact on the user experience, data sanity and can literally save us money.

For instance, have you ever noticed how many emails are not delivered due to the typos in the email address domain? Or how many SMS messages are hard bounced because of the wrong prefix or incorrect formatting?

Sometimes, validating the form based on the syntax is simply not enough to prevent customers from making mistakes.

And that’s where server-side validation comes in and saves the day!

Thanks to the Marketing Cloud built-in API, we will examine how to verify the email address against 3 different validators and prevent hard bounces from ever happening.

Libraries and methods

In order to set up a proper server-side validation, we first need to consider how to perform the client-side validation and choose a method for the requests to the Marketing Cloud server.

For the purpose of this exercice, we will use the following libraries and methods:

  • Bootstrap 5 for styling the form and validation messages.
  • Google Fonts for the style of the title.
  • Font Awesome for the icons.
  • Plain JavaScript for the client-side validation of the form.
  • AJAX (fetch and async/await) for real-time server-side validation.

How it works

Before diving in the code, we first need to understand how the real-time server-side validation works.

Obviously, it starts with the customer filling out the email address.

Then, as soon as email address field loses focus (which means that the customer clicked away from the field, possibly on the submit button), a request is sent to the Marketing Cloud server with the email address to verify if it will bounce when the form is submitted.

During this request, a spinning wheel is displayed, notifying the customer that he/she needs to wait for the process to end.

When the Marketing Cloud server responds, there are 2 possible outcomes:

  • If the server responded that the email will not be bounced, the form will either automatically submit the data or wait for the customer to click on the submit button.
  • If the server responds that the email will be bounced, a validation error message will be displayed on the form, informing the customer about what went wrong.

Now, let’s see how to translate all of this into JavaScript code and set up a Marketing Cloud page to become a server.

Diving into code

In order to build a server-side validation process, we’ll need to create 2 Marketing Cloud pages: a Form page that will contain the form and the Server page that will manage the AJAX requests from the form to validate the email address.

Form page

The Form page contains the form and the JavaScript code that sends requests to the Server page when a new email address is entered or when the form is submitted.

In regards to the HTML, let’s take a shortcut and use the example from the official Bootstrap 5 documentation about custom form styles.

<form 
    id="sfmc-form" 
    class="needs-validation my-4" 
    ref="form" 
    action="" 
    method="post" 
    novalidate
>
    <div class="row">
        <div class="col-12 mb-2 input-group-lg"> 
            <input 
                id="email"
                class="form-control"
                type="email"
                maxlength="254"
                placeholder="Your email..."
                required
                aria-describedby="validationEmail"
            >
            <div class="valid-feedback">
                Looks good!
            </div>
            <div id="validationEmail" class="invalid-feedback order-last">
                Please enter a valid email address.
            </div>
        </div>  
        <div class="col-12 mt-2 input-group-lg">   
            <button 
                id="btn"
                class="btn btn-primary px-5"
            >
                <i class="fas fa-arrow-right"></i> 
                <div class="spinner-border spinner-border-sm mb-1" role="status">
                    <span class="visually-hidden">Loading...</span>
                </div>
                Submit
            </button>
        </div>
    </div>
</form>

Now that we have our HTML code, let’s write some JavaScript and make it work with the Bootstrap form structure.

Grabbing the form elements

First things first, let’s grab the key form elements for our JavaScript code.

var form = document.getElementById("sfmc-form");
var email = document.getElementById("email");
var button = document.getElementById("btn");

Validation functions

We will need 2 separate functions for managing server-side email validation.

Check email validity

This function manages 3 different things:

  • Displays or hides the spinning wheel inside the submit button.
  • Triggers the asynchronous request to the Marketing Cloud server that will determine if the email will bounce.
  • Changes the email field validity and classes according the server’s response.

Please refer to the HTML5 documentation if you are unsure what “field validity” means in JavaScript.

async function checkEmailValidity(el) {

    button.classList.add('loading');

    var valid = await validateEmail(el.value);

    if(valid) {
        el.classList.add("is-valid"); 
        el.classList.remove("is-invalid");
        el.setCustomValidity("");
    } else {
        el.classList.remove("is-valid"); 
        el.classList.add("is-invalid");
        el.setCustomValidity("invalid");
    }

    button.classList.remove('loading');

    return valid;

}

Validate email

This function performs the AJAX request to the Marketing Cloud server, then displays the validation message under the email fields and returns a boolean value, to be used in the checkEmailValidity() function.

async function validateEmail(val) {

    var result = await fetch('{{ SFMC SERVER PAGE URL }}', {
        method: 'post',
        body: JSON.stringify({
            email: val
        })
    })
    .then(function(res) {
        return res.json();
    })
    .then(function(json) {
        if(json.message != null) validationEmail.textContent = json.message;
        return (json.status == 'OK');
    });

    return result;

}

Events

We will need 3 events to manage the interactions within the form: blur event, change event and submit event.

Blur event

Happens when the customer clicks away from the email field. If the click was not performed on the submit button, this event triggers the server-side email validation through the function checkEmailValidity().

email.addEventListener('blur', function (event) {
    if(event.relatedTarget != button) checkEmailValidity(this);
}, false);

Change event

Happens when the value of the email field changes (when the customer types something in the field). This event removes the the “is-invalid” class, that displays the error message under the field and resets the custom validity of the field.

Please refer to the HTML5 documentation if you are unsure what “custom validity” means in JavaScript.

email.addEventListener('change', function (event) {
        this.classList.remove("is-invalid");
        this.setCustomValidity("");
}, false);

Submit event

Happens then the customer tries to submit the form by clicking on the submit button or hits the “enter” key when the cursor is on the last form field.

This event performs the following tasks:

  • Adds the class “was-validated” on the form, which displays the validation messages.
  • Checks the validity of the form fields and performs server-side email validation asynchronously thanks to the checkEmailValidity() function.
  • Submits the data if the form validity checks are a success.
form.addEventListener('submit', async function (event) {

    event.preventDefault();

    form.classList.add('was-validated');

    if (!form.checkValidity() || await checkEmailValidity(email) === false) {
        console.error("form is invalid");
    } else {
        form.submit();
    }

}, false);

Now that we have our HTML and JavaScript in place, let’s create a Marketing Cloud server page.

Server page

The Server page is a regular Marketing Cloud page that receives the data from an external source, processes the data and returns a JSON response.

In our case, instead of data processing, we will be checking if the provided email address is going to bounce by making a request to the Marketing Cloud API for email validation.

In order to achieve this result, we will use the same code and Data Extensions from my previous article about managing REST API tokens and requests.

But in this case we will not stop at retrieving the REST API token: we will use that token to make a request to another endpoint, that will verify if the email address provided by the Form page will bounce.

Verify email address

The Marketing Cloud API for email validation can run the email address through 3 different validatos:

  • SyntaxValidator: verifies the structure of the email (name@domain.extension).
  • MXValidator: verifies the MX record (if the domain is valid).
  • ListDetectiveValidator: verifies if the email exists in the proprietary Marketing Cloud database of bad email addresses.

Note that the code returns a custom validation message for each validator.

function verifyEmailAddress(email, auth) {

    if(email === null || email.length === 0) {

        throw "Nothing to verify."

    }

    if (auth.token != null) {

        var requestUrl = auth.url + "address/v1/validateEmail";        

        var payload = {
            "email": email,
            "validators": [ 
                "SyntaxValidator", 
                "MXValidator", 
                "ListDetectiveValidator" 
            ]
        }

        var req = new Script.Util.HttpRequest(requestUrl);
            req.emptyContentHandling = 0;
            req.retries = 2;
            req.continueOnError = true;
            req.setHeader("Authorization", "Bearer " + auth.token);
            req.method = "POST";
            req.contentType = "application/json";
            req.encoding = "UTF-8";
            req.postData = Stringify(payload);

        var res = req.send();

        var result = Platform.Function.ParseJSON(String(res.content));

        if(result != null) {

            if(result.errorcode != null) {

                throw Stringify(result.message)
            }

            if(result.valid == false) {

                var res = "Blocked by " + result.failedValidation + ": ";

                if(result.failedValidation == "ListDetectiveValidator") res += "invalid email address"
                if(result.failedValidation == "SyntaxValidator") res += "invalid email syntax"
                if(result.failedValidation == "MXValidator") res += "invalid email domain name"

                return {
                    status: "NOK",
                    message: res
                }

            } else {

                return {
                    status: "OK",
                    message: "Email address is valid!"
                }

            }

        } else {

            throw "Could not perform verification."

        }

    } else {

        throw "Could not generate the access token."

    }

}

Full code

Form page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
    <title>Validate Email App</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
    <script src="https://kit.fontawesome.com/680fcb47ef.js" crossorigin="anonymous"></script>   
    <link href="https://fonts.googleapis.com/css2?family=DM+Mono&display=swap" rel="stylesheet">
    <style>
        h2 {
            font-family: 'DM Mono', monospace;
        }
        button .spinner-border {
            display:none;
        }
        button.loading {
            pointer-events: none;
        }
        button.loading i {
            display:none;
        }
        button.loading .spinner-border {
            display:inline-block;
        }
    </style>
</head>
<body>
<div id="app" class="container w-50">
    <div class="mx-auto my-5">
        <h2><i class="fas fa-envelope"></i> Subscribe to our Newsletter</h2>
        <form id="sfmc-form" class="needs-validation my-4" ref="form" action="" method="post" novalidate>
            <div class="row">
                <div class="col-12 mb-2 input-group-lg"> 
                    <input 
                        id="email"
                        class="form-control"
                        type="email"
                        maxlength="254"
                        placeholder="Your email..."
                        required
                        aria-describedby="validationEmail"
                    >
                    <div class="valid-feedback">
                        Looks good!
                    </div>
                    <div id="validationEmail" class="invalid-feedback order-last">
                        Please enter a valid email address.
                    </div>
                </div>  
                <div class="col-12 mt-2 input-group-lg">   
                    <button 
                        id="btn"
                        class="btn btn-primary px-5"
                    >
                        <i class="fas fa-arrow-right"></i> 
                        <div class="spinner-border spinner-border-sm mb-1" role="status">
                            <span class="visually-hidden">Loading...</span>
                        </div>
                        Submit
                    </button>
                </div>
            </div>
        </form>
    </div>
</div>

<script>

    var form = document.getElementById("sfmc-form");
    var email = document.getElementById("email");
    var button = document.getElementById("btn");

    email.addEventListener('change', function (event) {

        /* If the value changes, remove validation */

        this.classList.remove("is-invalid");
        this.setCustomValidity("");

    }, false);

    email.addEventListener('blur', function (event) {

        /* If the blur event click is not on the submit button, validate the email */

        if(event.relatedTarget != button) checkEmailValidity(this);

    }, false);

    form.addEventListener('submit', async function (event) {

        event.preventDefault();

        form.classList.add('was-validated');

        if (!form.checkValidity() || await checkEmailValidity(email) === false) {

            console.error("form is invalid");
            
        } else {

            form.submit();

        }

    }, false);

    async function checkEmailValidity(el) {

        button.classList.add('loading');

        var valid = await validateEmail(el.value);

        if(valid) {

            el.classList.add("is-valid"); 
            el.classList.remove("is-invalid");
            el.setCustomValidity("");

        } else {

            el.classList.remove("is-valid"); 
            el.classList.add("is-invalid");
            el.setCustomValidity("invalid");

        }

        button.classList.remove('loading');

        return valid;

    }

    async function validateEmail(val) {

        var result = await fetch('{{ SFMC SERVER PAGE URL }}', {
            method: 'post',
            body: JSON.stringify({
                email: val
            })
        })
        .then(function(res) {
            return res.json();
        })
        .then(function(json) {
            if(json.message != null) validationEmail.textContent = json.message;
            return (json.status == 'OK');
        });

        return result;

    }

</script>

</body>
</html>

Server page

<script runat='server'>

    Platform.Load('core', '1');

    var api = new Script.Util.WSProxy();

    var config = {
        endpoint: "https://{{ YOUR DOMAIN }}.auth.marketingcloudapis.com/v2/token",
        credentials: {
            "{{ BU ID }}": {
                "grant_type": "client_credentials",
                "client_id": "{{ CLIENT ID }}",
                "client_secret": "{{ CLIENT SECRET }}"
            }
        },
        storage: {
            de: "REST_Tokens",
            name: "Validate Email"
        },
        bu: Platform.Function.AuthenticatedMemberID(),
        localDate: DateTime.SystemDateToLocalDate(Now())
    }

    var postData = Platform.Request.GetPostData();

    var form = Platform.Function.ParseJSON(postData);

    try {

        if(!Platform.Function.IsEmailAddress(form.email)) {

            // Very the email address pattern before validating it on the server side

            var result = {
                status: "NOK",
                message: "Blocked by SyntaxValidator: invalid email syntax.",
                description: "No API call performed."
            }

        } else {

            var auth = retrieveToken();

            var result = verifyEmailAddress(form.email, auth);

        }

        Write(Stringify(result));

    } catch(error) {

        Write(Stringify({
            status: "Error",
            message: error
        }));

    }

    function verifyEmailAddress(email, auth) {

        if(email === null || email.length === 0) {

            throw "Nothing to verify."

        }

        if (auth.token != null) {

            var requestUrl = auth.url + "address/v1/validateEmail";        

            var payload = {
                "email": email,
                "validators": [ 
                    "SyntaxValidator", 
                    "MXValidator", 
                    "ListDetectiveValidator" 
                ]
            }

            var req = new Script.Util.HttpRequest(requestUrl);
                req.emptyContentHandling = 0;
                req.retries = 2;
                req.continueOnError = true;
                req.setHeader("Authorization", "Bearer " + auth.token);
                req.method = "POST";
                req.contentType = "application/json";
                req.encoding = "UTF-8";
                req.postData = Stringify(payload);

            var res = req.send();

            var result = Platform.Function.ParseJSON(String(res.content));

            if(result != null) {

                if(result.errorcode != null) {

                    throw Stringify(result.message)
                }

                if(result.valid == false) {

                    var res = "Blocked by " + result.failedValidation + ": ";

                    if(result.failedValidation == "ListDetectiveValidator") res += "invalid email address"
                    if(result.failedValidation == "SyntaxValidator") res += "invalid email syntax"
                    if(result.failedValidation == "MXValidator") res += "invalid email domain name"

                    return {
                        status: "NOK",
                        message: res
                    }

                } else {

                    return {
                        status: "OK",
                        message: "Email address is valid!"
                    }

                }

            } else {

                throw "Could not perform verification."

            }

        } else {

            throw "Could not generate the access token."

        }

    }

    function retrieveToken() {

        var request = Platform.Function.LookupRows(config.storage.de, "Name", config.storage.name);

        var result = request[0];

        if(result != null && (new Date(config.localDate) < new Date(result.ExpirationDate))) {

            return {
                "token": decryptSymmetric(result.access_token),
                "url": decryptSymmetric(result.rest_instance_url),
                "expires": result.ExpirationDate
            }

        } else {

            var result = requestToken();

            var upsert = storeToken(result);

            if(upsert > 0) {

                return result;

            } else {

                throw "Token not saved"

            }
        }
    }

    function requestToken() {

        var request = HTTP.Post(config.endpoint, "application/json", Stringify(config.credentials[config.bu]));

        if (request.StatusCode == 200) {

            var result = Platform.Function.ParseJSON(request.Response[0]);

            var parsedDate = new Date(config.localDate);

            var expirationDate = new Date(parsedDate.getTime() + (result.expires_in * 1000));

            return {
                "token": result.access_token,
                "url": result.rest_instance_url,
                "expires": expirationDate
            }

        } else {
            
            throw "Couldn't request the token. Status: " + request.StatusCode;
        
        }
    }

    function storeToken(result) {

        var rows = Platform.Function.UpsertData(
            config.storage.de,
            ["Name"], [config.storage.name],
            ["access_token", "rest_instance_url", "ExpirationDate"],
            [encryptSymmetric(result.token), encryptSymmetric(result.url), result.expires]
        );

        if(rows > 0) {
            
            return rows; 
            
        } else {
            
            throw "Token storage failed"
            
        }

    }

    /* HELPERS */

    function encryptSymmetric(str) {

        Variable.SetValue("@ToEncrypt", str)

        var scr = "";
            scr += "\%\%[";
            scr += "SET @Encrypted = EncryptSymmetric(@ToEncrypt, 'AES', 'pwd', @null, 'slt', @null, 'vec', @null)";
            scr += "Output(Concat(@Encrypted))";
            scr += "]\%\%";

        return Platform.Function.TreatAsContent(scr);

    }

    function decryptSymmetric(str) {

        Variable.SetValue("@ToDecrypt", str)

        var scr = "";
            scr += "\%\%[";
            scr += "SET @Decrypted = DecryptSymmetric(@ToDecrypt, 'AES', 'pwd', @null, 'slt', @null, 'vec', @null)";
            scr += "Output(Concat(@Decrypted))";
            scr += "]\%\%";

        return Platform.Function.TreatAsContent(scr);

    }

</script>

Conclusion

This article only covers the case of email hard bounces, but please feel free to apply the same method for phone numbers, postal codes, street names and other possible fields that can benefit from the server-side verification.

Considerations

This article doesn’t cover the topic of Marketing Cloud form handlers. Please click here if you wish to learn how to manually manage data coming from a Marketing Cloud custom form.

For some weird reason, Marketing Cloud API completely fails when the domain “mall” is used (as in name@mall.com).

Have I missed anything?

Please poke me with a sharp comment below or use the contact form.

Salesforce Marketing Cloud
Up Next:

How to create, store and use REST API tokens in Salesforce Marketing Cloud

How to create, store and use REST API tokens in Salesforce Marketing Cloud