Most static frontend apps rely on fetching data over the network to function. And since "all input is evil", we have to assert fetched data until we can trust them to work with our code. And that is why type safety in the runtime is important.
TypeScript is enough, right? Maybe not
Why TypeScript is not enough
What do we want on our API layer in the application. We want a few things:
- we want compile-time definitions to make sure our code behaves and builds correctly
- we want to trust the API schema is correct, in the runtime
- we want to handle errors when the schema is wrong in the runtime
TypeScript already gives us compile-time definitions, which is great. It allows us to catch logical errors during static analysis stages and ensures the correctness of our logic. However, it does nothing in the runtime because, once again, it is a transpiler that never reaches the runtime.
All we need now is a parsing layer for the runtime. This layer allows us to catch schema errors in our API responses and handles them accordingly, in the runtime.
What is the exact gap with runtime safety?
Take a CRUD application. It has a Single-Page-App, SPA, static frontend and a RESTful endpoint. Both of them are in separate repositories and do not share any interfaces. As a front-end developer, you have to define interfaces and write fetching logic for the SPA. But how can you assure that your frontend always has the correct schema?
- you can use code generation tools to generate TypeScript interfaces that you import into your SPA, assign to the corresponding functions in your API layer
- or you write them manually in the API layer
Either way, you would not get runtime safety.
What you need, is a step in your parsing step that parses your data against a pre-defined schema. It would show errors in a safe way. This way, you only have to guard against your API once instead of writing question mark dots (optional chaining) and inline type-checks all over your codebase. And this is where Zod comes in.
Zod schema: define an interface that checks your data
In this example, we are defining a Staff schema with two attributes,
userName before inferring a TypeScript type from it. We can use the TypeScript type for compile-time safety, but since the
Staff schema exists in the runtime, we can use it to assert runtime safety. Something like this.
getStaff uses a fetch or Axios API internally, the promise will throw an error when you have networking issues (e.g. HTTP400). But it doesn't detect issues in the API response body. That is why runtime safety is important. By parsing our response through the
StaffArray schema, we can assert its shape and set error messages accordingly.
What did we achieve in the end?
By doing a parsing step with a Zod schema, we can infer a TypeScript interface for compile-time safety, but we also get runtime safety since it handles the parsing and provides errors when our shape is wrong. Rather than waiting until the erroneous data hits our business logic and then we handle it at that level, we now can implement it trivially in our API layer.
- Compile safety is preserved without writing a lot of additional code
- Runtime safety is achieved without writing complex parsing or guard functions
A codesandbox example
To illustrate what I've written, here is a code-sandbox repo with that exact implementation.