> ## Documentation Index
> Fetch the complete documentation index at: https://flatfile.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 02: Adding Validation to Your Listener

> Enhance your listener with data validation capabilities to ensure data quality and provide real-time feedback to users.

In the [previous guide](/coding-tutorial/101-your-first-listener/101.01-first-listener), we created a Listener that configures Spaces and sets up the data structure. Now we'll add data validation to ensure data quality and provide helpful feedback to users as they work with their data.

<Note>
  **Following along?** Download the starting code from our [Getting Started repository](https://github.com/FlatFilers/getting-started/tree/main/101.01-first-listener) and refactor it as we go, or jump directly to the [final version with validation](https://github.com/FlatFilers/getting-started/tree/main/101.02-adding-validation).
</Note>

## What Is Data Validation?

Data validation in Flatfile allows you to:

* Check data formats and business rules
* Provide warnings and errors to guide users
* Ensure data quality before processing
* Give real-time feedback during data entry

Validation can happen at different levels:

* **Field-level**: Validate individual [Field](/core-concepts/fields) values (email format, date ranges, etc.)
* **Record-level**: Validate relationships between [Fields](/core-concepts/fields) in a single [Record](/core-concepts/records)
* **Sheet-level**: Validate across all [Records](/core-concepts/records) (duplicates, unique constraints, etc.)

## Email Validation Example

This example shows how to perform email format validation directly when Records are committed. When users commit their changes, we validate that email addresses have a proper format and provide helpful feedback for any invalid emails.

<Note>
  This approach validates Records as they're committed, providing immediate feedback to users. For more complex validations or when you need an object-oriented approach, we recommend using the [Record Hook](/plugins/record-hook) plugin.
</Note>

<Warning>
  If you use both [Record Hooks](/plugins/record-hook) and regular listener
  validators (like this one) on the same sheet, you may encounter race
  conditions. Record Hooks will clear all existing messages before applying new
  ones, which can interfere with any messages set elsewhere. We have ways to
  work around this, but it's a good idea to avoid using both at the same time.
</Warning>

## What Changes We're Making

To add validation to our basic Listener, we'll add a listener that triggers when users commit their changes and performs validation directly:

```javascript theme={null}
listener.on("commit:created", async (event) => {
  const { sheetId } = event.context;
  
  // Get committed records and validate email format
  const response = await api.records.get(sheetId);
  const records = response.data.records;
  
  // Email validation logic here...
});
```

## Complete Example with Validation

Here's how to add email validation to your existing Listener:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import api from "@flatfile/api";

  export default function (listener) {
    // Configure the space when it's created
    listener.on("job:ready", { job: "space:configure" }, async (event) => {
      const { jobId, spaceId } = event.context;
      try {
        // Acknowledge the job
        await api.jobs.ack(jobId, {
          info: "Setting up your workspace...",
          progress: 10,
        });
        // Create the workbook with sheets
        await api.workbooks.create({
          spaceId,
          name: "My Workbook",
          sheets: [
            {
              name: "contacts",
              slug: "contacts",
              fields: [
                { key: "name", type: "string", label: "Full Name" },
                { key: "email", type: "string", label: "Email" },
              ],
            },
          ],
        });
        // Update progress
        await api.jobs.update(jobId, {
          info: "Workbook created successfully",
          progress: 75,
        });
        // Complete the job
        await api.jobs.complete(jobId, {
          outcome: {
            message: "Workspace configured successfully!",
            acknowledge: true,
          },
        });
      } catch (error) {
        console.error("Error configuring space:", error);
        // Fail the job if something goes wrong
        await api.jobs.fail(jobId, {
          outcome: {
            message: `Failed to configure workspace: ${error.message}`,
            acknowledge: true,
          },
        });
      }
    });

    // Listen for commits and validate email format
    listener.on("commit:created", async (event) => {
      const { sheetId } = event.context;
      try {
        // Get records from the sheet
        const response = await api.records.get(sheetId);
        const records = response.data.records;
        // Simple email validation regex
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        // Prepare updates for records with invalid emails
        const updates = [];
        for (const record of records) {
          const emailValue = record.values.email?.value;
          if (emailValue) {
            const email = emailValue.toLowerCase();
            if (!emailRegex.test(email)) {
              updates.push({
                id: record.id,
                values: {
                  email: {
                    value: email,
                    messages: [
                      {
                        type: "error",
                        message:
                          "Please enter a valid email address (e.g., user@example.com)",
                      },
                    ],
                  },
                },
              });
            }
          }
        }
        // Update records with validation messages
        if (updates.length > 0) {
          await api.records.update(sheetId, updates);
        }
      } catch (error) {
        console.error("Error during validation:", error);
      }
    });
  }
  ```

  ```typescript TypeScript theme={null}
  import type { FlatfileListener } from "@flatfile/listener";
  import api, { Flatfile } from "@flatfile/api";

  export default function (listener: FlatfileListener) {
    // Configure the space when it's created
    listener.on("job:ready", { job: "space:configure" }, async (event) => {
      const { jobId, spaceId } = event.context;
      
      try {
        // Acknowledge the job
        await api.jobs.ack(jobId, {
          info: "Setting up your workspace...",
          progress: 10
        });

        // Create the workbook with sheets
        await api.workbooks.create({
          spaceId,
          name: "My Workbook",
          sheets: [
            {
              name: "contacts",
              slug: "contacts",
              fields: [
                { key: "name", type: "string", label: "Full Name" },
                { key: "email", type: "string", label: "Email" },
              ],
            },
          ],
        });

        // Update progress
        await api.jobs.update(jobId, {
          info: "Workbook created successfully",
          progress: 75
        });

        // Complete the job
        await api.jobs.complete(jobId, {
          outcome: {
            message: "Workspace configured successfully!",
            acknowledge: true
          }
        });

      } catch (error) {
        console.error("Error configuring space:", error);
        
        // Fail the job if something goes wrong
        await api.jobs.fail(jobId, {
          outcome: {
            message: `Failed to configure workspace: ${error instanceof Error ? error.message : 'Unknown error'}`,
            acknowledge: true
          }
        });
      }
    });

    // Listen for commits and validate email format
    listener.on("commit:created", async (event) => {
      const { sheetId } = event.context;
      
      try {
        // Get records from the sheet
        const response = await api.records.get(sheetId);
        const records = response.data.records;

        // Simple email validation regex
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

        // Prepare updates for records with invalid emails
        const updates: Flatfile.RecordWithLinks[] = [];

        for (const record of records) {
          const emailValue = record.values.email?.value as string; 
          
          if (emailValue) {
            const email = emailValue.toLowerCase();

            if (!emailRegex.test(email)) {
              updates.push({
                id: record.id,
                values: {
                  email: {
                    value: email,
                    messages: [{
                      type: "error",
                      message: "Please enter a valid email address (e.g., user@example.com)",
                    }],
                  },
                },
              });
            }
          }
        }

        // Update records with validation messages
        if (updates.length > 0) {
          await api.records.update(sheetId, updates);
        }

      } catch (error) {
        console.error("Error during validation:", error);
      }
    });
  }
  ```
</CodeGroup>

<Note>
  **Complete Example**: The full working code for this tutorial step is available in our Getting Started repository: [JavaScript](https://github.com/FlatFilers/getting-started/tree/main/101.02-adding-validation/javascript) | [TypeScript](https://github.com/FlatFilers/getting-started/tree/main/101.02-adding-validation/typescript)
</Note>

## Testing Your Validation

### Local Development

To test your Listener locally, you can use the `flatfile develop` command. This will start a local server that will listen for Events and respond to them, and will also watch for changes to your Listener code and automatically reload the server.

```bash theme={null}
# Run locally with file watching
npx flatfile develop
```

### Step-by-Step Testing

After running your listener locally:

<Card title="Testing Steps">
  1. Create a new space in your Flatfile environment
  2. Enter an invalid email address in the Email Field
  3. See error messages appear on invalid email Fields
  4. Fix the emails and see the error messages disappear
</Card>

## What Just Happened?

Your Listener now handles two key Events:

1. **`space:configure`** - Sets up the data structure
2. **`commit:created`** - Validates email format when users commit changes

Here's how the email validation works step by step:

### 1. Listen for Commits

This listener triggers whenever users save their changes to any sheet in the workbook.

<CodeGroup>
  ```javascript JavaScript theme={null}
  listener.on("commit:created", async (event) => {
    const { sheetId } = event.context;
  ```

  ```typescript TypeScript theme={null}
  listener.on("commit:created", async (event) => {
    const { sheetId } = event.context;
  ```
</CodeGroup>

### 2. Get the Records

We retrieve all records from the sheet to validate them.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const response = await api.records.get(sheetId);
  const records = response.data.records;
  ```

  ```typescript TypeScript theme={null}
  const response = await api.records.get(sheetId);
  const records = response.data.records;
  ```
</CodeGroup>

### 3. Validate Email Format

We use a simple regex pattern to check if each email follows the basic `user@domain.com` format.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  for (const record of records) {
    const emailValue = record.values.email?.value;
    if (emailValue && !emailRegex.test(emailValue.toLowerCase())) {
      // Add validation error
    }
  }
  ```

  ```typescript TypeScript theme={null}
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  for (const record of records) {
    const emailValue = record.values.email?.value as string;
    if (emailValue && !emailRegex.test(emailValue.toLowerCase())) {
      // Add validation error
    }
  }
  ```
</CodeGroup>

### 4. Add Error Messages

For invalid emails, we create an update that adds an error message to that specific field.

<CodeGroup>
  ```javascript JavaScript theme={null}
  updates.push({
    id: record.id,
    values: {
      email: {
        value: email,
        messages: [{
          type: "error",
          message: "Please enter a valid email address (e.g., user@example.com)",
        }],
      },
    },
  });
  ```

  ```typescript TypeScript theme={null}
  updates.push({
    id: record.id,
    values: {
      email: {
        value: email,
        messages: [{
          type: "error",
          message: "Please enter a valid email address (e.g., user@example.com)",
        }],
      },
    },
  });
  ```
</CodeGroup>

<Info>
  You can apply different types of validation messages:

  * **`info`**: Informational messages (mouseover tooltip)
  * **`warn`**: Warnings that don't block processing (yellow)
  * **`error`**: Errors that should be fixed, blocks [Actions](/core-concepts/actions) with the `hasAllValid` constraint (red)
</Info>

### 5. Update Records

Finally, we send all validation messages back to the sheet so users can see the errors.

<CodeGroup>
  ```javascript JavaScript theme={null}
  if (updates.length > 0) {
    await api.records.update(sheetId, updates);
  }
  ```

  ```typescript TypeScript theme={null}
  if (updates.length > 0) {
    await api.records.update(sheetId, updates);
  }
  ```
</CodeGroup>

## Next Steps

Ready to make your Listener interactive? Continue to [Adding Actions](/coding-tutorial/101-your-first-listener/101.03-adding-actions) to learn how to handle user submissions and create custom workflows.

For more detailed information:

* Understand Job lifecycle patterns in [Jobs](/core-concepts/jobs) and [Spaces](/core-concepts/spaces)
* Learn more about [Events](/reference/events)
* Organize your Listeners with [Namespaces](/guides/namespaces-and-filters)
* Explore [plugins](/core-concepts/plugins): [Job Handler](/plugins/job-handler) and [Space Configure](/plugins/space-configure)
* Check out [Record Hook](/plugins/record-hook) for simpler Field-level validations
