Zod in Action: Building a Resilient Data Validation Pipeline
Tags: webdev, javascript, typescript, programming
Published: 2023-10-15 13:24:44

Welcome back to the second part of my blog series on `Zod`, the powerful data validation library in `JavaScript!`. In our previous blog, we explored the fundamentals of Zod and its extensive capabilities for ensuring data integrity and validation. Today, we will take our knowledge a step further and delve into a practical example that showcases the true power of `Zod`. We will walk through the seamless integration of a `React` frontend with a `Node.js` backend, implementing robust data validation using Zod's expressive schema definitions. Get ready to witness the magic of Zod as we build an application that not only validates user input but also performs mock logic to deliver a seamless user experience. So, let's roll up our sleeves and dive into the wonderful world of Zod! 🚀💪


# Set up the backend server:

1. Install the necessary dependencies by running `npm install express` and `npm install zod`.

2. Create a new file called server.js and add the following code:

```
const express = require('express');
const app = express();
const { ZodSchema, z } = require('zod');

// Define the schema for validating the form data
const formDataSchema = ZodSchema.object({
  name: z.string().min(3, 'Name must be at least 3 characters long'),
  email: z.string().email('Invalid email format'),
  age: z.number().positive('Age must be a positive number'),
});

// Define a mock logic function
function performMockLogic(data) {
  // Perform some actions using the data
  console.log('Performing mock logic with data:', data);
  return { success: true };
}

app.use(express.json());

// Define a POST endpoint for receiving the form data
app.post('/api/submit', (req, res) => {
  const formData = req.body;

  try {
    // Validate the form data against the schema
    const validatedData = formDataSchema.parse(formData);

    // Perform the mock logic using the validated data
    const result = performMockLogic(validatedData);

    // Send the result back to the client
    res.json(result);
  } catch (error) {
    // If validation fails, send an error response
    res.status(400).json({ error: error.message });
  }
});

// Start the server
app.listen(5000, () => {
  console.log('Server is running on port 5000');
});
```

Now lets understand this code!

First, we import the necessary dependencies. We require the express library, create an instance of the application, and import the ZodSchema and z objects from the zod library, which enable us to define and validate schemas.

Next, we define the formDataSchema using ZodSchema.object(). This schema represents the structure and validation rules for our form data. In this example, the schema defines three fields: name, email, and age. Each field has specific validation rules. For instance, the name must be at least three characters long, the email must have a valid email format, and the age should be a positive number.

After defining the schema, we create a function called performMockLogic(), which represents a mock logic function where we can perform actions using the validated data. In this case, we log a message to the console and return a success object.

Then, we set up our Express application to use JSON parsing middleware with app.use(express.json()). This middleware enables the server to parse JSON data sent in the request body.

We define a POST endpoint at /api/submit to receive the form data. Inside the endpoint, we access the form data from req.body.

To validate the form data against the specified schema, we use formDataSchema.parse(formData). If the validation succeeds, the validated data is stored in the validatedData variable.

Next, we invoke the performMockLogic() function with the validated data as an argument and store the result in the result variable.

Finally, we send the result back to the client as a JSON response using res.json(result). If any validation errors occur during the parsing process, an error message is sent as a JSON response with a status of 400.


# Set up the React app:
1. Create a new React app by running `npx create-react-app` frontend.
2. Go into the created frontend directory by running `cd frontend`.
3. Install the necessary dependencies by running `npm install axios`.
4. Replace the contents of `src/App.js` with the following code:

```
import React, { useState } from 'react';
import axios from 'axios';

const App = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState('');
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();

    const formData = { name, email, age };

    try {
      // Send the form data to the backend
      const response = await axios.post('/api/submit', formData);

      // Set the received result in the state
      setResult(response.data);
      setError(null);
    } catch (error) {
      // Set the error message in the state
      setError(error.response.data.error);
      setResult(null);
    }
  };

  return (
    <div>
      <h1>Form Example</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          placeholder="Name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <br />
        <input
          type="text"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <br />
        <input
          type="number"
          placeholder="Age"
          value={age}
          onChange={(e) => setAge(e.target.value)}
        />
        <br />
        <button type="submit">Submit</button>
      </form>

      {error && <p>{error}</p>}

      {result && (
        <p>
          Mock logic performed successfully! Result: {result.success.toString()}
        </p>
      )}
    </div>
  );
};

export default App;
```

First, we import the necessary dependencies. We import React and specific functions from the react package, such as useState, which allows us to manage component state. We also import axios, an HTTP client for making API requests.

Next, we define a functional component called App. Inside the component, we declare state variables using React's useState hook. We have name, email, and age variables to hold the form input values. The result variable stores the response from the backend API, while the error variable stores any error messages.

We define an handleSubmit function that will be called when the form is submitted. It prevents the default form submission behavior and creates a formData object with the current values of name, email, and age.

Inside the try block, we use Axios to make a POST request to the /api/submit endpoint with the formData. The response is stored in the response variable.

If the request is successful, we set the received result in the component state using setResult(response.data) and clear any previous errors by setting setError(null).

If an error occurs, the code in the catch block is executed. We set the error message from the response in the error state using setError(error.response.data.error), and reset the result state to null.

In the component's return statement, we display a simple form with inputs for name, email, and age. Each input is bound to its respective state variable and updates its value through the onChange event handler, implemented using arrow functions.

When the form is submitted, the handleSubmit function is called. If there is an error state, we display the error message using {error && <p>{error}</p>}. If a result is available, we display a success message along with the result using {result && <p>...</p>}.

5. Start the development server by going back to the root directory of the project.
Run `npm start`.

Now, you should have the React app running on `http://localhost:3000`, and the backend server running on `http://localhost:5000`. When you submit the form, the data will be sent to the backend, validated using Zod, and the mock logic will be performed. You will see the result or any validation errors displayed in the React app. 🚀🖥️


# Conclusion
In conclusion, this blog post explored the integration of Zod, React, and Axios to handle form submissions in a web application. By leveraging Zod's powerful schema validation capabilities, we can ensure that the data submitted by users meets the specified requirements and is safe to process on the server. React's useState hook enables us to manage and update the form input values, while Axios simplifies the process of sending HTTP requests to the backend API. The code example demonstrated the usage of Zod-centric form handling, emphasizing the importance of validation and error handling. By following this approach, developers can enhance the user experience by providing robust and secure form submissions in their applications.