Creating Zod Input Validation Error after tRPC entry
I've got a ct3a project, with all the bells and whistles (including tRPC and zod). In addition, on the frontend part of the app I've set up a couple of forms with
react-hook-form
, and I am sharing the zod schemas being used with tRPC.
What I'm looking to achieve, is AFTER the input validation by the tRPC router mutation has been completed, and the code has moved on into the logic within the mutation itself, to be able to send back an input validation error and set these errors in react-hook-form
.
This would be the business logic:
1. User begins registration flow.
2. User enters their email
and username
along with other fields.
3. React-hook-form uses a shared schema (with tRPC) to validate items such as minLength
, isEmail
, regex for special characters
, etc.
4. User submits valid the form-data to tRPC via a mutation.
5. tRPC accepts and validates the incoming input (since the same schema is being shared on the frontend).
6. In the mutation, the username
field is checked in the database to find if it already exists.
7. If username
already exists, a BAD_REQUEST
error (or something equivalent in the 400 range) would be sent back to frontend, which then can be inserted into react-hook-form.
P.S. I have no clue if this (https://trpc.io/docs/v9/error-formatting#all-properties-sent-to-formaterror) plays into what I'm looking for.Error Formatting | tRPC
The error formatting in your router will be inferred all the way to your client (& React components)
41 Replies
I am using the Form component from advanced guide in
react-hook-form
(https://react-hook-form.com/advanced-usage/#SmartFormComponent).
I use my onSubmit
prop of the component to return either void/null when there's no error, or MyAppError[] (which has the same type as validation errors from Zod +tRPC)
Then I just set those errors to those returned from my onSubmit
prop.
Example implementation:
Advanced Usage
Performant, flexible and extensible forms with easy-to-use validation.
I also created an error class for faster implementation for my custom errors (you can uimplement your own formatter)
Example usage:
throw new BadReqTrpcError("message", "username")
I think I get it, basically, the mutation returns either
null/void
or error[]
. If error[]
, then setting the errors onto in hook-form. Seems cool.yup
or like not the mutation
but the function onSubmit which runs the mutation
but it's basically the same thing 90% of the time
Would you mind sharing the/a repo with this?
I don't think I have a public repo for that but I can do something quickly for you if you'd like to wait ~10 - 15 minutes
No stress, it was mostly for seeing whats in the utils and if this Error class needed to be added anywhere else in tRPC.
Nope, utils is just for translation stuff mostly (i've got project that uses i18n)
And you just throw the error like any other one
Gotcha gotcha
It's my simplest form component if you'd like to see one:
Cool stuff, I've got a ct3a app open right now and I'll give it a shot.
Good luck!
1 question, the return type of the utils function is
string
yea?Yeah, it was just changing key like
auth.user_with_same_email
to corresponding message in chosen language (eg. "User with the given e-mail adress already exists."). Don't pay attention to it, I just copied it from my multi-lingual appThanks, testing now.
@Piotrek Ran into abit of a hurdle
This is the error on the client I received (screenshot)
Throw the Error using this
throw new BadReqTRPCError("Message is invalid and a paragraph", "message")
Am I missing anything here? any client-side stuff I'm messing upBasically, the path of the invalid field isn't returned.
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
could you send the code that's failing?
User.setUserPublicMessage
is the place according to your errorSure thing, this basically me intentionally throwing the error to see the response in the client.
Oh, so that's what you want, no? 😄
Now you can just use the error and return it in the onSubmit function
Yea, basically a hacky debug so I can understand how I'd map the errors to react-hook-form after being piped from trpc
Then handle it in the form component
My issue there is that the response/error from trpc doesn't contain the path/field onto which the errorMessage needs to be applied onto.
Oh, i forgot about my errorFormattter on the tRPC side
You'll have to handle the path before returning the error
Oh, that probably makes sense
You have to pass errorFormatter to your
.create({ errorFormatter })
function called on theinitTrpc
This the appRouter?
My formatter method is that:
I just append new
errors
field onto the response with my shape
No, the t
variable that's being created with the initTRPC
object
I don't know where it is in t3-app, I'm using a Turborepo hahaI'm talking about this guy
Btw I'm terrible at explaining stuff so sorry if it takes like 5 minutes to explain something easy haha
1. import for ReturnedError is from where?
2. I assume context is the tRPC context?
No sweat man, this is some super helpful stuff
For future ref, in create-t3-app its here
Oh it's the v9
That's why I was confused 😅
Yeah, the context is the tRPC one
I'm using the V10 version tho so not sure if the function will look the same
You should have access to the context tho
The returned error is just my type, probably something like
{message:string;path:string}
I think this may be in play here. Since even though the type import for
ReturnedError
is missing, the formatter should return the new shape, but this is not the case.In tRPC the error.message is just a string and you can't parse it it looks like
You'll just have to play around with your error formatter
EDIT: Got the ReturnedError type by taking
type ReturnedError = ReturnType<typeof formatZodIssues>;
Yup, will crack at it for a while.Good luck and... Night/whatever is right now where you live!
Night!
Its 1:44 AM here in Sri Lanka
It's 10:14 pm in Poland, so night too
Fixed 🎉
Thanks a bunch @Piotrek
No problem!
Good job!