Typing API Responses With Zod
Tags: javascript, typescript, react, dotnet
Published: 2022-07-15 04:54:07

Have you ever needed to synchronize types in your frontend app with the backend API?

If you ever had an API action defined like that in your controller:
{% embed https://gist.github.com/dsibinski/bf658760106de7269ebf3d0f351c0411 %}

and fetched this data using TypeScript in the following way:
{% embed https://gist.github.com/dsibinski/44438baa3b6ece08a191b0e918c01a77 %}

at some point, you probably also experienced the desynchronization of backend (C#, in our example) and frontend (TypeScript) types definitions. What if someone has changed the C# version of `UserViewModel`, but no one corrected its TypeScript’s equivalent?

Your TypeScript fetching code will tell nothing about that. **There will be no error, even though the fetched data doesn’t match the expected `UserViewModel` type**.

I’ll try to address this issue in this article 🙂 Let’s see how typing API responses with `zod` can help us here.

## Synchronization of Backend and Frontend API Typings
First, why would we want to keep the backend and frontend models in sync?

For me, that’s the purpose of using TypeScript. **We want our code to be as well-typed as possible**. 

For instance, we normally want the data displayed to the user to be fully typed. TypeScript enhances our programming experience by providing us with typing information. Thanks to that, we know _what is what and what contains what_. We also express what types of data we expect in particular cases.

The APIs mostly return JSON data, which can be anything. Because of that, it is much easier to have the data returned from the API fully typed in TypeScript. Thanks to that, **we know what properties are available on the data models received from the API and whether we can use and display them to the users**.

The sample code used within this article is available [on GitHub](https://github.com/dsibinski/DotNetReactZodApp). We will use ASP.NET Core (C#) and React (TypeScript) apps as examples.

## Models Synchronization Example
As we saw in the beginning, a classic example is an API controller that returns a strongly-typed data:

{% embed https://gist.github.com/dsibinski/237ce046ebdfcae6b4b7485f8c8c2006 %}

The returned data type is a collection of `UserViewModel` objects. Here’s the C# definition of this type:

{% embed https://gist.github.com/dsibinski/a6921cad261fb274900c991956a18f5a %}

Its equivalent is also defined on TypeScript side:
{% embed https://gist.github.com/dsibinski/d03ea25ba0be55e645a0b6fe35595f47 %}

## Usage in TypeScript
Cool. With this simple code, we can create a `usersService.ts` file and fetch our users’ data from the API. Notice how we make this call strongly typed:

{% embed https://gist.github.com/dsibinski/b49678cb4bb3d9ed23b9e59ecd58b595 %}

Everything looks legit. We can use the data retrieved from the API in `UsersList` component and everything is nicely typed:


![UsersList React component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a4vssbf7xu5a0cd56ie1.png)

The data is even perfectly displayed:


![Table with users rendered on the web page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y3o7ykryqtxvlqthc2uz.png)

So, what can go wrong here? 🤔

## The Problem – Typings’ Desynchronization
Let’s say that a backend developer implements a requirement to rename “loyalty points” into “fidelity points”. Easy. (S)he renames `LoyaltyPoints` property in the C#’s `UserViewModel` to `FidelityPoints`.

The new C# model looks as follows:
{% embed https://gist.github.com/dsibinski/7e0f39f3c7ffb2010000bb0ea1629701 %}

Nice! The backend dev is a very good programmer, so (s)he even launches the React web application to make sure that everything still works correctly and there are no errors in the dev console:


![Table of users rendered on the web page with empty "Loyalty points" column and no errors on Chrome DevTools console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/naao5kw5sl4l0itmsnjy.png)

After a quick look, everything looks awesome. The users list is displayed, there are no errors in the console. Apparently, these test users don’t have any loyalty points assigned – that’s why the empty values in “Loyalty points” column. What’s more, translators will update the column’s translation later. We are good! Let’s go on prod! 😎

I guess you already know what went wrong here. **API definition changed, but TypeScript didn’t inform us about that** 😔 Our `UserViewModel` still uses the old property name:


![UserViewModel typescript type definition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ciu5ilzdu9ehp5t7jdoo.png)

However, it still works. When rendering the `UsersList`, we simply get `undefined` in place of `loyaltyPoints`:


![UsersList rendering at runtime. user.loyaltyPoints property is always undefined](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6ikh6k3vsn8o0fitmon.png)

In the end, [this is all JavaScript there](https://www.codejourney.net/2018/11/10-most-shocking-javascript-features-for-csharp-developers/). What’s interesting, the renamed `fidelityPoints` property is already there at runtime:


![At runtime we can see the new fidelityPoints property already present](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4i7huf3sso1c18pudwsu.png)
but no one cared about it 😔

**With the current solution, we will never be informed soon enough about API models changes in our React application**. In the best case, we’ll get an `undefiend` or `null` error when clicking through the app. However, it’s usually an end user who finds such problems on production. This is definitely not what we want 😶

We can solve this problem by typing API responses with [zod](https://github.com/colinhacks/zod). Let’s now see how to do that.

## The Solution – zod
Our remedy – `zod` – is quite a [decent npm package](https://www.npmjs.com/package/zod) with ~600k weekly downloads. Its GitHub page advertises the library as _TypeScript-first schema validation with static type inference_.

You can definitely do many things with zod. It can be used together with libraries like [react-hook-form](https://react-hook-form.com/) to perform complex forms validation. However, in our case, **we’ll treat `zod` as a solution for better typings in TypeScript**.

### Adding zod to React app
First, let’s install `zod` into our React application:

```
npm i zod
```

### First Schema Definition with zod
With `zod`, we define our types in a slightly different way. Instead of creating a `type` or `interface` directly, we first create a _schema_. In our case, we can define a `UserViewModelSchema` using `z.object` creator function:

{% embed https://gist.github.com/dsibinski/9c7759dff996babccf1b14da276d76c8 %}

Few interesting parts here:
* _Line 2_: notice how `zod` helps us to define types like `Guid` with built-in schemas like `uuid()`
* _Line 8_: first, I used `AddressViewModelSchema` here. This is a custom schema of an `AddressViewModel` object, which is another type used internally in `UserViewModel`. You can use such custom schemas in other schemas. Also notice the `nullable()` call here, which makes the address property nullable

First step done – we have our `UserViewModelSchema`. But can we use it instead of `UserViewModel` type? Not really. Schema is used for validation purposes only. We still need the `UserViewModel` TypeScript’s type.

### Inferring Type From zod’s Schema
Fortunately, `zod` comes with a handy `z.infer` function that allows us to _infer_ the type from the schema.

Finally, the `userViewModel.ts` file looks as follows:
{% embed https://gist.github.com/dsibinski/64cb91fade703c4bbf6ade4c6ad84d19 %}

We can use the exported `UserViewModel` type as previously used type. It’s an equivalent to the previous, "classic" type we had defined, but this time inferred from `UserViewModelSchema`.

### Validating API Eesponses With zod Schema
One last step is to make use of `UserViewModelSchema`. Let’s modify the `getAllUsers` function from `usersService` to validate the data received from the API against our schema:

{% embed https://gist.github.com/dsibinski/ac10bdf2e256f549939d6eeed68067aa %}

Notice the usage of `z.array`. This function call tells `zod` to validate an array of objects meeting the rules defined by `UserViewModelSchema`, not a single object.

Now, let’s run our React app and see what happens when we click the “Fetch users” button:

![zod throws schema validation error on Chrome dev tools console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ypn6937o90mln23oya1.png)

This is awesome! Exactly what we wanted – a **schema validation error for API response**. Notice how the **error message precisely points to the missing (or wrong, in other cases) property**. It tells us we expected a `number` called `loyaltyPoints`, but instead we received `undefined`. The reason for this error message is that the `loyaltyPoints` field is `Required` in our schema.

After renaming `loyaltyPoints` to `fidelityPoints` in `UserViewModelSchema` and updating the `UsersList` component accordingly, everything works fine again.

We are now fully typed and prepared for the future, in case an issue with desynchronization of frontend and backend typings happens again 🚀

## Summary
Today, we’ve seen how typing API responses with `zod` can help us detect frontend and backend models desynchronization. Schema validation throws errors when the data doesn’t match its expected shape.

Remember that `zod` is an extended library with many options. I recommend [exploring them](https://github.com/colinhacks/zod) on your own. An interesting feature we didn’t cover in this article is [strict mode](https://github.com/colinhacks/zod#strict), which doesn’t allow additional fields not present in the schema definition when validating the data object.

The open question remains whether to use schema validation on production. One could think that it’s better to not throw any validation errors, _because JavaScript may just work_. However, I think that throwing an error is always better than _silently_ letting things through. An error lets programmers, automated tests or manual testers to detect the issue before the end user does 😉

You can explore the complete code presented in this article [here](https://github.com/dsibinski/DotNetReactZodApp).