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...
}
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...
};
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
};
});
How This Works:
-
Defining the Schema:
- We define an object schema using Zod's
z.object()
method that contains the fieldscageNumber
,coloDetails
,country
, andcity
. - The
country
field must match one of the entries in theCountryEnum
. - The
city
field is a string that cannot be blank.
- We define an object schema using Zod's
-
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.
- We use the
-
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.
- After validation, we use
Example Input and Output
Let’s consider the following input:
const siteAddress = {
cageNumber: "123",
coloDetails: "abc",
country: CountryEnum.Denmark,
city: "CPH",
};
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'
}
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:
- Fail Fast: Validate your data as early as possible. Catching invalid data before it enters your system prevents errors from propagating.
- Use Meaningful Error Messages: Provide clear and actionable feedback for invalid input, helping users correct their mistakes quickly.
- Keep Validation Logic Close to the Data: Where possible, enforce validation rules at the schema level, ensuring they are applied consistently across the system.
- Leverage Enums and Constants: Use enums and constants to restrict values to a predefined set, reducing the likelihood of invalid inputs.
- 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)