Validate HTTP API Response with zod, fetch and axios

In this lesson we learn that TypeScript cannot guarantee correct shapes of responses. That's due to TS performing checks in compile-time, while the response will be availbale in runtime - when TS doesn't exist anymore.

In some situations, such as HTTP requests, runtime type checks such as zod are essential for providing quick feedback around data-related errors.

Understand where are the possible mistakes to make while using TypeScript with HTTP calls. Use fetch and axios along with zod schema checks.

Also, note that similar mistakes or mistakes could happen while using web storage (e.g. localStorage)

Share with a coworker

Transcript

[00:00] The situations when TypeScript is incapable of guaranteeing type safety to our codebase is most often loading data from external sources such as HTTP requests, web sockets, web storage, databases and so on and so forth. What's common for all these is that the shape of the data in runtime doesn't come from our code base but from the external data source and hence the TypeScript compiler cannot guarantee its correctness. So let's start with writing an example HTTP function and I'm going to toggle the booking example object and the booking schema. So we're going to start with a fetch data function which is going to use the native fetch API internally And we're going to use the API that is available online, the jsonplaceholder.typico.com, which basically exposes some REST resources that are being reset every few minutes. So it's very good to have a play.

[01:00] So we're going to run it against the API URL and we're going to fetch the post slash one resource. And what is obvious is that whatever the post is, it's going to be totally different than a room booking schema. These structures are going to be totally totally different and what we want to illustrate over here is that TypeScript will have no means, no way to figure out that what we will actually get and what we claim that we are going to get because this is what we're going to annotate over here is totally different. TypeScript cannot see it. TypeScript has no way to figure it out.

[01:39] So let's make the function async. Let's also create the response variable which is going to await the fetch call and let's return the response.json call. Now what is important is that response.json call returns a promise of any. This is not a mistake by any author of the types. So again TypeScript has no means to figure out what is going to be the runtime value and TypeScript exists and works in compile time, so this is before the thing will actually be executed.

[02:11] So the promise of any can be casted into a promise of a booking. Why? Because booking is either a subtype or a supertype of any and hence this could be asserted. But the function result could also be annotated with the promise.booking. Why?

[02:30] Because booking is essentially a subtype of any. So everything from TypeScript perspective is correct, but we know this is total rubbish. So let's run this function. Let's execute that there is a fetch data. And when we have actually the result we're just going to console log it.

[02:52] So let's see what is going to happen. Let's run ts-node and this is going to be the API response validation file and what we can see is the data returned from JSON placeholder which includes the user ID, ID title and body whatever that is and we can see that this is clearly totally different than the booking schema. Now the whole point of introducing runtime validation like Zod is that even though we claim that the structure that we expect is of a certain shape, but there could be any change on the backend that hasn't been yet synchronized with whatever our data structure is, the one that we expect. So if there is any mismatch, if there is any difference that we are not yet aware of, then we would like to have it automatically reported, automatically figured out. So what we can do essentially is that we can, before we do the .thenConsoleLog, we can add additional step which is going to run the validation itself.

[04:01] So if this is the result what we can do is basically parse the result. So again let's say that this is what we're expecting so that would be the booking schema and we're going to parse the result over here which is expected to fail. Fail very badly actually. There is actually a whole collection of errors. So there is an invalid type, invalid type, invalid type, invalid type, et cetera.

[04:24] But that's the whole point. So this is something that TypeScript is incapable of figuring out because this happens in runtime and hence we are running a runtime validation. Now this is not specific to Fetch API so let's introduce Axios, a popular library that is responsible for running HTTP requests. So let's also create the fetch data function, which is going to rely on Axios API, and it's going to be very similar. So we're going to run axios.get, and in the meantime, we can import the Axios function.

[05:07] So let's introduce import Axios from Axios. So we've got the Axios.get and we're going to run the exactly same request. We're going to use the async await API over here so let's make this function async. Return the response.data. Now this one is not a promise but still what we've got over here is an unknown.

[05:35] Now, not an any, but a known. So let's see where does this come from. So if we take a look at axios.get, then we will see that there is a generic type over here that we don't define and actually there is no way for TypeScript to figure out what is this t. So a default value for the type parameter is being used and the default for TypeScript type parameters is unknown. But what we can do actually over here is that we can say that hey I'm expecting a booking object over here.

[06:08] And finally FetchDataAxios it's claiming to return the booking object which is of course wrong. So what we can do is to run fetch data axis. And again, then what we can do is to run the booking schema parse with the result. So let's just pass the result over here. And yeah, of course we have to run the function and this would be the result and let's also Break it into the next line and I can just comment this out so that when we are running this file we will see only the Axios API running and we see exactly the same errors because essentially this is the same API though it doesn't matter which API we're using whether it's Fetch or Axios and whether it's HTTP requests, WebSockets, WebStorage, databases, etc.

[07:06] TypeScript again has no means to figure out in compile time what is going to be the exact shape in runtime. So we have to make some assertions over here like this one or like this one. This depends on the API. Another common situation is when we're using web storage such as local storage. So let's say that we have a key that is holding let's say the state of our application and what we're going to do is to get the value from a certain key, so that would be get item over the local storage object and let's say that the key over here is the state.

[07:48] So the nature of local storage is that all keys, if they exist, they hold a string value. If they don't exist then what is being returned is a null. Now the whole point most often is that we're going to stringify a big JSON object whether it's nested or flat, whether it's big or small, but we're going to stringify it. So if this thing exists, we would need to run an if statement, So let's just simplify things and exclude the null just for the sake of simplicity. So now this is a string.

[08:23] Let's also parse the JSON that we would keep over here. So that would be json.parse over what we've had over here. Now what does JSON.parse return? Any. For the very same reason.

[08:37] Because TypeScript has no means to figure out what is going to be the data structure in runtime. And yet again this is just a question of whatever we claim here but we could be wrong. So in all these cases it's a good idea to include or at least to consider including a runtime validation.