DEV Community

Prince Raj
Prince Raj

Posted on

Data Validation in Your Backend: A Practical Guide

Data validation is one of the most crucial steps in building a robust backend system ensuring data flowing through the system is accurate, consistent, and adheres to predefined formats and preventing incorrect or malicious input from causing havoc down the line.

In this article, I'll walk you through a concrete example of how to leverage TypeScript with Zod, an excellent validation library, to handle city and country validation in a backend system. We'll dive into how we can use enums, schema validation, and input transformation to ensure the data is consistent and correctly structured before persisting it into the database.

Why is Data Validation Important?

Data validation is the process of ensuring that the input data conforms to the expected structure and content. Failing to validate data can lead to security vulnerabilities, bugs, and data integrity issues. For example, imagine a system that accepts user details including location. Without proper validation, the system might accept an invalid city, which could cause downstream errors, such as failed transactions or mismatched reports.

By introducing data validation:

  • Prevention of Bad Data: Invalid or malicious data gets filtered out before it can impact your system.
  • Improved User Experience: Validation can prevent users from submitting incorrect information, providing meaningful feedback.
  • Enhanced Security: Proper validation reduces the risk of injection attacks or other vulnerabilities.
  • Consistent Data Integrity: The data stored in your system remains structured and accurate, minimizing bugs.

Now, let's look at how we can implement validation for country and city data using Zod.

Real-World Use Case: Validating City and Country

In our application, we want to validate that the city provided by the user belongs to the selected country and, in the process, convert city codes (AITA codes) to their full names before storing them in the database. Here's how we can achieve that with Zod.

Setting Up Your Enums and Schema

First, we define the list of countries and their respective cities using AITA codes. We create a CountryEnum to represent the list of valid countries and a countryCityMap object to store the mapping between AITA codes and full city names for each country.

export enum CountryEnum {
  Afghanistan = "Afghanistan",
  Albania = "Albania",
  Algeria = "Algeria",
  Andorra = "Andorra",
  Angola = "Angola",
  // More countries...
}
Enter fullscreen mode Exit fullscreen mode

Next, we define a countryCityMap that contains country names as keys and a nested map of AITA codes to city names as values.

export const countryCityMap: Partial<Record<CountryEnum, Record<string, string>>> = {
  Denmark: {
    AAR: "Aarhus",
    AAL: "Aalborg",
    BLL: "Billund",
    CPH: "Copenhagen",
    // More cities...
  },
  // More countries and their cities...
};
Enter fullscreen mode Exit fullscreen mode

Building the Zod Schema for Validation

Now, we build the Zod schema for our site address. This schema will validate that the city provided by the user exists within the selected country, and then it will transform the city code into the full city name before storing it in the database.

import { z } from 'zod';

export const siteAddressZodSchema = z
  .object({
    cageNumber: z.string().optional(),
    coloDetails: z.string().optional(),
    country: z.nativeEnum(CountryEnum),
    city: z.string().min(1, "City cannot be blank"),
  })
  .refine(
    (data) => {
      const cityMap = countryCityMap[data.country];
      return cityMap && Object.keys(cityMap).includes(data.city);
    },
    {
      message: "Selected city does not belong to the selected country",
      path: ["city"],
    }
  )
  .transform((data) => {
    // Convert city code to the full city name after validation
    const cityName = countryCityMap[data.country]?.[data.city];
    return {
      ...data,
      city: cityName, // Replace AITA code with the city name
    };
  });
Enter fullscreen mode Exit fullscreen mode

How This Works:

  1. Defining the Schema:

    • We define an object schema using Zod's z.object() method that contains the fields cageNumber, coloDetails, country, and city.
    • The country field must match one of the entries in the CountryEnum.
    • The city field is a string that cannot be blank.
  2. Refining the City-Country Relationship:

    • We use the .refine() method to ensure that the selected city exists within the provided country. The function checks if the city's AITA code is found in the corresponding country’s city map.
  3. Transforming the Input:

    • After validation, we use .transform() to replace the AITA code with the full city name. This ensures that the data stored in the database is user-friendly and standardized.

Example Input and Output

Let’s consider the following input:

const siteAddress = {
  cageNumber: "123",
  coloDetails: "abc",
  country: CountryEnum.Denmark,
  city: "CPH",
};
Enter fullscreen mode Exit fullscreen mode

This input represents a site located in Copenhagen, Denmark. The city is provided as the AITA code CPH. After passing through the schema validation and transformation, the output will be:

{
  cageNumber: '123',
  coloDetails: 'abc',
  country: 'Denmark',
  city: 'Copenhagen'
}
Enter fullscreen mode Exit fullscreen mode

The system has validated that CPH is a valid city code for Denmark and then converted it into its full city name, Copenhagen.

Best Practices for Implementing Data Validation

Here are a few best practices to keep in mind when implementing data validation in your backend:

  1. Fail Fast: Validate your data as early as possible. Catching invalid data before it enters your system prevents errors from propagating.
  2. Use Meaningful Error Messages: Provide clear and actionable feedback for invalid input, helping users correct their mistakes quickly.
  3. Keep Validation Logic Close to the Data: Where possible, enforce validation rules at the schema level, ensuring they are applied consistently across the system.
  4. Leverage Enums and Constants: Use enums and constants to restrict values to a predefined set, reducing the likelihood of invalid inputs.
  5. Centralize Validation: Maintain validation rules in a single place, such as with Zod schemas. This keeps your code clean, consistent, and easier to maintain.

Conclusion

Data validation is a vital step in creating reliable and secure applications. Here we’ve demonstrated how to validate city and country data using TypeScript and Zod, ensuring that the data stored in the database is accurate and meaningful.

Implementing strong validation early in the data pipeline helps prevent downstream bugs and ensures data integrity throughout your system. Whether you’re building a small API or a complex microservices architecture, robust data validation will protect your backend from a range of issues, including security vulnerabilities and data inconsistency.

DO COMMENT YOUR THOUGHTS ON IT :)

Top comments (0)