Validating Remix Form Data Using Zod and TypeScript in Action Functions
If you love TypeScript it's likely in part because of the safety that using an excellent type system will give you, and once you start using types and feel that level of comfort, you'll start wanting to use it everywhere.
Remix actions allow you to use standard web APIs to handle form data and capture user input in the context of your web server while coding in the context of the pages and components that the user is interacting with.
Using the schema-validation library Zod let's you combine the ergonomics of Remix actions, with standard web APIs, and the safety of TypeScript.
It's great!
With your Remix apps, when you POST
a form, your ActionFunction
has formData
available on the request.
This is actually kind of a radical approach in the world of modern JavaScript frameworks. The FormData
that remix is posting with the request is the standard web API for form data that you can look up on MDN. Instead of giving you some new API to learn, Remix strives to utilize standard APIs.
Of course, form data isn't typed, so how can we bring TypeScript into the mix and create types from our form data?
Say hello to Zod.
TypeScript-first schema validation with static type inference
This means that you define a schema, feed any plain old JavaScript object into it, and Zod will validate the data against the schema and return a typed object that you can use. Here's what it looks like in the context of a Remix route:
As is normally the case in Remix, our page exports a component. In this case the component that is exported is named Index
. That component is relatively simple and exports a Form
component with two labeled inputs and a submit button. The Form
uses the post
method, but you'll notice that it doesn't have an action
attribute.
From the docs:
Most of the time you can omit this prop. Forms without an action prop (
<Form method="post">
) will automatically post to the same route within which they are rendered. This makes collocating your component, your data reads, and your data writes a snap.
There are cases where you might want to post to another route than the one you are on, but Remix by default will assume the ActionFunction
is in the same route as the form.
When the user hits Submit
the form will post
to the ActionFunction
. This gives you access to the request
which contains the form data:
FormData
isn't a plain object and has a low level API for interacting with the data that is good to understand, but the above uses Object.fromEntries
to create a plain-old JS object from the form data which is what you want in this case.
Here's where Zod comes in:
With Zod, you build a schema based on the shape of the data that you expect. There is a deep API for building schemas, and this is just the tip of the iceberg, but it's already super handy.
Using subscriberSchema.parse
and passing it our JavaScript object validates that the object conforms to the schema and gives us a type inferred object that we can have more confidence in using. Once we've validated the new subscriber, we can save their information and do other work before redirecting them to another route to display a confirmation.
You could also return JSON and access that data on the same page with useActionData
if that better suited your needs.
If the data doesn't pass the validation Zod will throw an error, so you'll want to handle that as well:
In this case we log the error to our (server) console and update the current route with an error parameter. This isn't very robust, and I'd recommend consider using useActionData
with an error object to give the user more specific feedback about what went wrong.
The error
object Zod produces contains an array called issues
that can be used to be more specific and thorough in your Zod error handling.
This is just the beginning of what Zod and Remix action functions can do!
If you'd like to learn more about Remix, check out this free course from Kent C. Dodds.
Matt Pocock's interactive video tutorial covering the basics of Zod is a great resource if you'd like to learn more about Zod!