In our projects, we use AdminJS, an external library that provides a GUI for managing database records. It's a great tool for rapidly creating CRUD interfaces for our clients.
However, we ran into some issues with poorly typed code inside the library that made it difficult to integrate with our own custom logic. This is where Zod, a validation library, came to the rescue.
Zod enabled us to create robust type definitions and validate data against those definitions. This helped us to avoid runtime errors and catch issues early in development.
In this article, we'll take a look at how Zod helped us to deal with the poorly typed code in AdminJS and how it improved the overall quality of our code.
The article highlights how the utilization of Zod, a validation library, significantly improved type safety within a project. By addressing issues stemming from poorly typed code in AdminJS, Zod enabled the creation of robust type definitions and data validation processes. This proactive approach helped prevent runtime errors and enhanced the overall quality of the codebase.
When using external libraries, it's not uncommon to run into issues with poorly typed code. This was the case with AdminJS, a library that we used in our projects to provide a GUI for managing database records. While AdminJS is a great tool for rapidly creating CRUD interfaces for our clients, it also presented some challenges.
One of these challenges was creating schemas for the objects that we received from AdminJS. The library provides a Record<string, any> type, which is not particularly useful when it comes to type safety. We needed to define more robust type definitions for our data to avoid runtime errors.
This is where Zod, a validation library, came into play. We used Zod to create schemas for the objects that we received from AdminJS, which enabled us to catch errors early in development and avoid issues in production.
The code snippet above demonstrates how we defined a schema for a dashboard object that we received from AdminJS. We used Zod's object method to create an object schema with three properties: title, project, and type. We also used Zod's nativeEnum method to create an enumeration schema for the type property, which accepts two specific string values.
By creating these schemas with Zod, we were able to define the exact shape of the data we expected to receive, and catch any errors if the data did not match that shape. This helped us to ensure the quality of our code and avoid any issues caused by the poorly typed code in AdminJS.
In the previous section, we defined a schema for the entire payload that we receive from AdminJS. However, in some cases, we might only be interested in a specific part of the payload, or we might not need all the fields from the payload. In such cases, it is useful to create sub-schemas, which define a subset of the original schema.
ZOD provides several methods to create sub-schemas, such as pick, omit, and partial.
In the code snippet provided, we are using these methods to create three different sub-schemas from the original DashboardPayloadSchema:
Creating sub-schemas can be useful when we want to validate only a part of the object, or when we want to reuse some of the fields in a different schema. It can also help in simplifying the validation logic and making it more readable.
One of the key features of Zod is the ability to refine schemas with custom validation logic. The refine method can be used to add validation rules to a schema, allowing you to ensure that data meets specific requirements before it is processed.
In the following code snippet, we use refine to ensure that the title field in our DashboardPayloadSchema is no longer than 255 characters. If the title is too long, Zod will throw an error with a custom message.
The refine method can also accept asynchronous functions, as shown in the following example. Here, we use refine to ensure that the project field in our schema exists in a database. If the project does not exist, Zod will throw an error with a custom message.
While validation ensures that the received data conforms to the schema, sometimes you might need to transform the data to a different format or structure. For example, you might need to extract a certain substring from a string or make a database call to fetch additional data based on a received value.
Zod provides the transform() method that allows you to modify the validated values before returning them. The transform() method accepts a synchronous or asynchronous function that takes the validated value and returns the transformed value.
In the following code snippet, we define a DashboardProject schema that refines the received project string to ensure that it starts with the "PR_" prefix. Then, we use the transform() method to remove the prefix before returning the value.
You can also use an asynchronous function with transform() to perform more complex operations, such as making a database call:
In the above example, the getProjectObjectFromDB() function is an asynchronous function that fetches the project object from the database based on the received project string. The transform() method applies this function to the validated value and returns the result.
In the above code snippet, the infer method is used to automatically infer the type of the schema defined by DashboardPayloadSchema. The inferred type is then assigned to a type alias called Dashboard. This allows us to use the inferred type throughout our codebase without having to manually define it.
Next, the code exports a type guard function called isDashboard. A type guard is a function that checks if a value is of a certain type at runtime. In this case, the isDashboard function checks if the provided payload conforms to the Dashboard type, by attempting to parse the payload using the DashboardPayloadSchema. If the parsing succeeds, the function returns true, indicating that the payload is a valid Dashboard. If the parsing fails, the function returns false.
Using type guards like isDashboard can help catch type errors at runtime and make our code more robust, especially when working with data from external sources like APIs or databases where the shape of the data may not be known in advance.
In this article, we explored how Zod, a validation library, came to the rescue when integrating AdminJS, an external library with poorly typed code. AdminJS provides a Record<string, any> type, leading to type safety issues. Zod helped us create robust type definitions and validate data against those definitions, catching errors early in development.
We defined a schema for a dashboard object using Zod's object and nativeEnum methods, ensuring the expected data shape. We also created sub-schemas with pick, omit, and partial for specific parts of the payload. Custom validations were added with the refine method to enforce requirements like string length and database existence.
Zod's transform method allowed us to modify validated values, and we learned how to infer types using infer, creating type guards to catch type errors at runtime. Overall, Zod improved code quality and reduced runtime errors, making our integration with AdminJS more efficient and reliable.
The Brainhub promise
Every year, Brainhub helps 750,000+ founders, leaders and software engineers make smart tech decisions. We earn that trust by demystifying the technology decision-making process based on practical software engineering experience.
Top reads this month
Get smarter in engineering and leadership in less than 60 seconds.
Join 300+ founders and engineering leaders, and get a weekly newsletter that takes our CEO 5-6 hours to prepare.
No previous chapters
No next chapters