Okay, it's time to take a look at zod. The first time I encountered zod was because it was a recommendation on the GitHub dashboard, the second time was the nomination for the OS awards for the "Productivity Booster" category. Initially, I had neutral impressions about this library, but it clicked after having a conversation with a colleague.
The colleague raised the question of why we don't validate the response bodies of our HTTP requests. The main reason was that, if the backend does this for its input, then why shouldn't it be done in the frontend? This was coming after having fixed a few issues because the types on the backend and the front end have become outdated.
After giving it some thought, I have to agree with him. Though the reasons why are different. On the backend, we want to validate the input (the source, and the content) because everyone can send a request. In the front end, the source is already trusted, but we want to parse the content. This is to be notified about any discrepancies we don't expect.
If you want to manually "parse" a response body, things can quickly become messy, involve a lot of code, and you still have to maintain and keep the interface and the validation logic in sync.
This is where I think zod is a game-changer.
Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple string to a complex nested object.
Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures.
Let's take a look at some code.
To start things off, let me first describe where things could go wrong. As an example, the following snippets contain code to retrieve a user within an Angular application using the HTTP client.
Before diving into the service and the component, we first need to define the
This interface is used in the application to add a type to the inputs and outputs of the methods.
In the snippet below, the Angular service is created to start an HTTP request to retrieve the user's information.
User interface is used to add the type of the response, an
Next, the service is invoked within the component.
Within the component, we don't need to explicitly add the
User interface because
userService.getUser() is statically typed and thus knows that the returned value is a
Most of the time, this code just works. But did you spot situations where this could potentially fail? If you have been in a similar situation, you have probably spotted the problem.
username property is
undefined this code throws an error.
Another reason why this code could fail is when the server returns another object, or when we've used the wrong type within the Angular service by accident. This can quickly happen but can take more time than expected to spot the root cause of the problem.
In a way, TypeScript offers us a false sense of security. TypeScript is a statically typed language, but its runtime objects could be anything. This is a common mistake, and we've all bumped against it.
Now that we know the potential issues, let's explore how zod can offer a solution.
Just like before, let's start to define the
User interface, this time by using
Instead of directly defining the interface, we're obligated to create a schema first.
Just like the interface, the schema holds the contract of the
The difference between the two is that we're now using the zod utilities (
zod.number()) instead of the types that TypeScript provides.
Also, the schema is not an interface nor a type, but it's a variable.
To use it as a type, we'll have to convert it into a type.
Luckily, zod also has a helper method (
zod.infer()) to do this.
There's still one more difference between the two. The interface becomes a type and is inferred based on the schema.
So far, nothing much has changed. Simply said, the interface is replaced with a type, and the application continues to work as before.
Let's see what we can do with a zod schema, because this is what makes zod useful. zod gives us utility methods to parse (and validate) an object instance at runtime based on a schema.
To verify if an instance matches with the schema definition, use the
safeParse methods that are available on the schema variable.
The difference between the two is that
parse throws an error when it fails, and
safeParse returns a
A quick demo based on the
User type that we created before:
As you can notice in the above snippets, when the instance doesn't match with the schema, zod prints out a handy error with useful information. It's clear why the object is not valid, the message includes the property and the reason.
zod has a lot of possibilities, and you can do much more than this. But, this post is about where to use zod in your application. If you want to know more about it, check out the detailed zod documentation and examples.
Now that we know how to parse an object with zod, let's see how, and more important where, we can use it in our application. Like I said in the beginning, we want to verify the response bodies of HTTP requests.
To not pollute the application, this is best done in the HTTP service, right after we receive the response.
Take a look at the next snippet, which is utilising the RxJS
tap operator to pass the response body of the request to the
While this works, it also makes the application very strict.
Every time that the response is not what's expected, the
parse method throws an error.
This might impact the user experience of the application.
This might be good, but I would prefer to loosen the validation a little for the production build.
To keep the behavior consistent, and because we don't want to write the same code for all the HTTP requests, I abstracted the validation logic into a specialized RxJS operator called
The operator takes a schema as an argument, and uses the environment to run a different implementation:
- in development: the
parsemethod is used, thus an error is thrown when the body is invalid. This makes the developer aware that something needs to be looked into.
- in production: the
safeParsemethod is used, meaning that the body is let through as is when it's invalid. BUT, the invalid body is logged (together with the parsing issue(s)).
The refactored service now uses the custom
parseResponse operator instead of
tap and looks like this:
And that's it! We are now finished to validate the incoming user, and the component can safely consume the user object.
If you wonder about the impact of this additional logic, I got you covered. For small collections, the performance impact is negligible.
Here's a benchmark to parse a collection of a normal-sized model.
In this post, we've written a solution to verify that response bodies have the desired contract. We did this by introducing zod into the codebase, which provided us a way to easily parse objects. In our case, the response bodies of HTTP requests.
Adding zod is a small effort, instead of directly creating a TypeScript interface (or type), we:
- create a zod schema
- infer the TypeScript type based on the zod schema
Using the zod schema also has a minor impact when done correctly. Only the services are affected, the other parts of the application don't need to know anything about the zod API, and thus continue to work the same way as before.
To parse the object within the services we created a reusable RxJS operator
parseResponse, which contains all the logic to parse the response bodies.
This way, it's just a one-liner that needs to be added after each HTTP request.
While it has a minimal cost, the benefits of using zod and parsing the response bodies are huge:
- the components can safely consume the objects returned by the services; the application isn't polluted with additional checks
- we get a descriptive message when the response body doesn't match with the expectations immediately when the data is received, compare this to an obscure error when the application would crash instead
There's one caveat though. As far as I know, the refactoring method to quickly rename properties of an interface directly in the whole codebase is not possible anymore.
Side note link
You can choose to have a simple schema that simply checks the data types, or you can create a complex one that also verifies the content of the response body (e.g. numbers that must be greater than and/or lower than an expected value). I prefer to keep the schema simple. Creating a complex schema opens up the door to adding business logic to the schema. This can be useful for other use cases, but not for the one we've discussed in this post.
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.