#14 | Validation & Flash Storage - AdonisJS 7
Chapters17
This chapter introduces the concept of data validation and sets up the context for validating user-provided data, highlighting why ensuring correct shape and constraints is important.
Defensive data handling in AdonisJS 7 uses Vine.JS validators, request-based validation, and helpful UI feedback with flash messages and components.
Summary
Adocasts’ #14 leans into robust validation in AdonisJS 7 by showing how to wire a validator with Vine.JS, and how to use it across store and update actions. The tutor creates a challenge validator (challenge.ts) in app/validators and defines field rules for text and points with type, min/max constraints, and optional nullable behavior. The lesson demonstrates validating request data with await request.validate using the new validator, which automatically handles errors and returns typed, safe data. It also covers manual validator usage, route params, and how the framework surfaces errors via flash messages, including input errors and a general errors bag. Practical debugging tips appear as you submit invalid data, inspect flash messages, and switch to cleaner template helpers like input error and the built-in old() helper to repopulate fields. Finally, the video demonstrates integrating AdonisJS’ field components (at field, root, at input control) to streamline forms, including displaying error states and preserving user input after failed validations. The overall message: validation is declarative, user-friendly, and deeply integrated with AdonisJS’ request lifecycle and UI helpers.
Key Takeaways
- Vine.JS lets you define a single validator (challenge.ts) with text: string (min 5, max 255) and points: number (min 1, max 100).
- The request.validate method automatically runs the validator against all request data (body, query, params, cookies, headers) and returns typed results.
- Optional vs nullable control whether empty fields are omitted or kept as null in the validated data; this distinction matters for update vs create flows.
- Validation failures throw an exception that AdonisJS handles via content negotiation, returning 422 with input errors when the client expects JSON and redirecting on HTML forms.
- You can validate route parameters and headers by extending the Vine validator with a params key and similar helpers.
- There are UI helpers like input error and the input errors bag (and the newer input error tag) to render per-field validation messages cleanly.
- The old() helper automatically repopulates fields with previously submitted values from flash messages, improving UX after validation errors.
Who Is This For?
Laravel/AdonisJS developers upgrading to AdonisJS 7 or building robust form validation, with a focus on practical, framework-native validation and user feedback mechanisms.
Notable Quotes
""Validation allows us to ensure that the data being provided not only adheres to a shape that we expect, but also additional constraints on a per property basis that we expect as well.""
—Intro to why validation is used and what it enforces.
""We can say that this should come through as a string value and we can hold it to a minimum length of five and a maximum length of 255""
—Defining text field rules in the validator.
""The points should instead be a valid number value""
—Casting and validating numeric input.
""When we submit an HTML form, it will redirect us back to the page we were previously on. When using an application JSON accept header type, it will return back a 422 status code with our errors in the body""
—Error handling behavior on validation failure.
""Adonis has provided us a helper method called old on our state""
—Preserving input after failed validation.
Questions This Video Answers
- How does AdonisJS 7 handle validation errors in HTML form submissions vs JSON API requests?
- What is Vine.JS in AdonisJS and how do you define a validator for multiple actions?
- How can I customize error display in an AdonisJS 7 app using flash messages and input error tags?
- What’s the difference between nullable and optional in AdonisJS validators?
- How do you validate route parameters and headers with Vine.JS in AdonisJS 7?
AdonisJS 7Vine.JSAce CLIvalidationrequest.validateflash messageserrors baginput errorold()Edge.js components
Full Transcript
Now, as mentioned in the last lesson, we want to be defensive with the data that we're taking in that's been provided by our users. The best way to do that is with validation. Validation allows us to ensure that the data being provided not only adheres to a shape that we expect, but also additional constraints on a per property basis that we expect as well. So, to start, we can go ahead and jump back into our terminal here. Ctrl + C to stop it. I'm going to clear that out. And we can use the ACE CLI to make a new validator.
We can have multiple validators inside of a single validator file. So we can call this just challenge to put any challengebased validations that we need inside of our application in this one file. And this will be placed inside of our app validators directory inside of a new file called challenge.ts. All right, I'm going to go ahead and boot up my server again while I'm in here. And we can close that back out. And let's jump into our app validators folder and take a look at our new challenge.ts file. And you'll notice it's rather simple from the get-go.
All it's doing is importing Vine.JS. And Vine.JS is AdonisJS's validation system. Like Edge.js, it too is homegrown by the AdonJS core team. So we can use Vine to export a constant and give our validator a name. So I'm going to call this challenge validator because the shape of our challenge is rather straightforward and the same between both our create and update operations. So we'll have just this one validator for both. Then we can create a validator by doing vine.create and providing in an object of the properties that we want to validate inside of our request.
The keys of our object here are the field names we're submitting within our form and the values of our object are then the validation rules for that field. So we'll have a text field that we're sending up and then we'll use vine to describe the shape of this field as well as additional limitations that we want on it. So we can say that this should come through as a string value and we can hold it to a minimum length of five and a maximum length of 255 to ensure that it fits inside of our databases column whenever we get to that point.
Then we also have our points field which currently with how we're getting our data is coming up as a string which resulted in that weird concatenation for our total available points of the number plus string value just being joined together. Using our validator, then we can say that our points should instead be a valid number value. And similar to how we're casting our route parameters, this will take in that string value, ensure it is a valid number, and then cast it to a number value that we get back from our validated data. Again, we can also specify additional constraints to this as well.
We don't want to allow negative points, for example. So, we can set a minimum of one and a maximum of 100 to ensure that we don't have a mega challenge that's worth way too many points, skewing the weights of our challenges as a whole. Alternatively, there is also a range validation rule that accepts in a min and max value that you can use as an alternative to the split min and max rules as well. Now, by default, any field that we provide within our validator is going to be a required field unless we explicitly mark it as optional or nullable.
So, for example, if we wanted our users to be able to clear out a text name or leave a text name empty, we could set this to nullable. And now they would be able to send up null or an empty field value for the text of our challenge. There is also an optional method here as well that does vary slightly from nullable. So, they both allow a user to send up an empty value for the field. But if optional is used and the field is empty, then the property will be omitted from our returned validation data.
If nullible is used, it will be included with our return validated data with a null value. So nullable is going to allow the user to clear the field out on an update where that wouldn't be the case with optional. In our case though, we do need both of these required. So we're going to leave those off of this al together. That's all that we need to do to define a validator. Next, let's make use of it inside of our challenges controller. So we need this on both our store and update methods. And the easiest way is to use it directly off of our request.
So we can do const data equals and this is an async operation. So we'll do await request dot and there's a validate using method on here that allows us to just pass a validator in. And then the method will automatically create a validator instance and pass it all of the data available on our request. That includes our body data, query string data, route parameters, cookies, and even headers. So within here, we just want to pass in our challenge validator. Hit tab to autoimp import that. And now we're validating our data inside of our requests body that we're sending up with our form against our challenge validator and getting that data back inside of our data variable.
And you'll note that that's also typed appropriately as well as described by that validator. We have a text of type string and points of type number. meaning that we can now get rid of these two request input calls and use our data directly in a much safer fashion. So we'll just merge our data in to that object that we're pushing into our challenges for our store. And then we can jump down to our update and do the same. We can just replace this request only with an await request validate using and pass in that challenge validator just like so.
When we use the validator off of our request as we are here, this will automatically handle error handling for us. if the validation should fail and we're going to take a look at that next but I also want to show how you can manually use a validator as well should you need to provide manual data into it outside of your request scope. So to do that there is actually a validate method off of the validator directly. So if we do challenge validator dot you'll notice that we have a validate method just right there that we can manually provide data that will be passed into the validator.
In this particular case, none of the data from our request will automatically be passed in, nor will error handling automatically occur. If the validator fails, it will throw an error. So, if we need to handle that in some other way than just returning that error, we're going to need to wrap this in a try catch and handle that ourselves. All of that happens automatically on our behalf using the request directly, though. Now before we get to that, I did also mention that you can validate route parameters, query strings, cookies, and even headers inside of your validators using the request directly.
When the request passes those into the validator, they are under specific properties. So our body is passed in directly and accessible exactly as we have it here. But if we want to validate route parameters, that's going to be put inside of a params object inside of our validations. So if we need to do that, we can do vine object on a key called params to validate any of our route parameters. So if we have an ID there, we can do vine.number to validate any of the route parameters as a number and requiring an ID route parameter to boot.
And that's the same for query string data, cookies, and our headers as well. Those all work relatively similar should you need them in any case. Those will also come back from the validated data scoped as such. So we would have data.params params data.query string data.cookies or data headers. Okay, we don't need any of that. Again, just showing that as an example. What we now need to do is take a look at the error handling of this. So, if we jump back into our browser and let's try to create a brand new challenge here. Let's provide our form some invalid data.
So, we're requiring at least five characters, I believe, is what we set it to on our text. So, if we set that to four and give our point value something invalid there, we max that out at 100. So, if we provide 5,000, that's going to fail our validation. So, if we hit create here, you'll notice that we're redirected right back to where we were and our form is emptied out. Why? Well, when our validation fails, it will throw an exception. As we mentioned previously, since we're validating with our request here, our exception handler will automatically and conveniently handle this for us using content negotiation to determine how to respond.
When we submit an HTML form, it will redirect us back to the page we were previously on. When using an application JSON except header type, it will return back a 422 status code with our errors in the body. So, where can we find the errors here for our form submission? Well, let's go ahead and jump into our create page. So, we'll scroll on down to our challenges create.edge page here, and let's just dump out our state. So, between our div and our form here, we'll do an at dump and plop our state on the page.
If we give that a save and jump back into our browser, we should now see a dump appear right before our form. Let's fix our dump carrots really quick so that we have them working properly going forward. So, we can do that by jumping into our resources CSS app CSS file. Uh, and then we're just going to do a search. So, command F to search inside of this file for button. And we're specifically looking for one that has a width of 100% set on it. Perfect. We should be able to fix our carrots here by doing a not to negate from this button rule our dumper toggle element.
With that saved, we should be able to close out of our CSS file then back into our create page. and then jump back into our browser. And yeah, that looks a lot better. Great. On our states, if we dig into this, we should see a property, and it might be a little bit more towards the bottom, called flash messages. Flash messages are messages sent by our server for this single request. If we go ahead and expand this, we're going to see values. And if we dig into those values, it's currently going to be empty. Let's go ahead and try submitting our form one more time with our invalid state.
So, we'll do test and 5,000 as our points. create our challenge to resubmit our form to have our validation fail once more. And now if we dig back into our object and take a look at those flash message values one more time, you're going to notice that we now have some state here. This is where we can find the contextual information about our validation failure. The errors bag is where we'll find general exceptions and errors. And the input errors bag is where we'll find specific validation errors for our input fields. If there is a property for a field within here, then it means that that field has failed validation and adon.js will give us an array of errors for the rules that that property failed as validation for.
So for example, here on our text, we can see that the text must have at least five characters. For our points, it should be something like a max. So the points field must not be greater than 100. Meanwhile, our errors bag contains an evalidation error noting the general exception that has been thrown with a message. which the form could not be saved. Please check the errors below. So, input errors bag are specific input errors and errors bag are general errors thrown by our server. Additionally, here within our flash message values, you will also note that we have the previously submitted values for both of our fields as well.
So, how can we use all of this? Well, let's first take a look at the prototype here. And you'll notice that we have a couple of helper methods directly off of our flash message property on our state. We have a simple getter on whether or not the flash message state is empty. We have a get function to get flash messages for a specific property. There's also has all to object and to JSON helpers as well. Typically for validations, get and has are the two that you'll most likely use. Get to get the actual error and has to determine whether or not it actually has an error inside of our flash messages.
So, if we go and jump back into our text editor inside of our create page underneath our labels, we can add in an at if conditional, reaching through to the flash messages state and using the has helper to reach into our input errors bag object to see if it has a text property. And this allows us to provide a string representation of that object path and it will handle that appropriately. So, for example, we could do input errors bag.ext to reach through the input errors bag object into the text property inside of it to determine if it has an error in there.
If it does, then we can display a div with the flash messages.get and then get those input errors from the input errors bag for our text property. And since this is an array, you can either loop over those errors or you can simply just join them as a comma delimited list as we'll do right here. So we can go ahead and give this a copy then and apply it to our points field as well. So if we drop that in there just like so. And then we can switch text to points for both the has and get methods.
Okay, so let's give that a save. Jump back into our browser then. And let's try this one more time with invalid state still. So we'll do test and 5,000. Hit create challenge and fantastic. We now have our error messages showing below our fields. Now this is a common enough situation. So Adonjs has provided some utility tags to help us get at this data in a cleaner fashion. So let's replace our if with this helper tag called input error. And as you might guess by the name, it is scoped specifically to our flash messages input errors bag.
So we don't need to explicitly specify that here. We just need to provide the property name of the key we're after inside of that object, which is text in this case. Additionally, this will inject a messages object into the context of this input error, automatically providing us the flash message errors inside of the key that we're after and it prefixes this message property with a dollar sign. So, we can do dollar sign messages dot and then apply our comma delimited join there. So comparing the two syntaxes, you can see one is definitely much cleaner and easier to use than the other.
So let's apply this to our points as well. So input error again, we don't need the input errors bag designation anymore. And then we can replace this here with dollar sign messages join to add in our common delimited list. Of course, you can also loop over these as well. Then we no longer need this end if, but rather just an end since we're no longer doing a conditional check for both of these. And I also have a dangling and parenthesis there on both as well that I'll get rid of. So great, with that, we should be able to give this a save.
Jump back into our browser. And let's give this a test one more time. So test 5,000. Enter. Fantastic. Finally, it would be great if we automatically give our user back their previously submitted values here inside of our form. So we can use our flash messages again to do that by populating a value onto our input fields. And we could either read that directly from our flash messages using get. But again, this is a common enough scenario. So, Adonjs has provided us a helper method called old on our state. And this will just merely do that automatically for us.
It will read from those flash messages if it's provided. So, we can provide in the field that we're after here, which is points. And then we can scroll on up to our text input and do the exact same thing. So, we'll just use interpolation, call the old method, and pass in text as the field we're after there. with that. If it finds an old value on our flash messages for the field that we're after, it will set that value in our field. If not, then it will return undefined as we see here in our text field.
To fix that, we can go ahead and add in an empty string then as the default value instead of having that come through as undefined. So, with that saved, then now we're all good to go. All right, let's enter test and 5,000 to fail out one more time. Hit enter. And not only do we get our error messages still, but we now also get back the previously submitted values as well. Great. So, our form's looking good. Let's go ahead and make it a little bit better because the starter kit also comes with components wrapping everything that we've done here so far in a pretty little bow.
So, for example, we can go ahead and instead of individually doing our text and input and value and all of that stuff, use at field. And you'll notice that we have an error label and root from our autocomplete. We want to start with the root. This sets up the context that should be injected for the state of our input that we're going to be defining. And inside of here, we need to give it a name. So, this is going to be our field name of text. Everything we use inside of this route then has that contextual information that it sets up in this component, allowing us to then use field.l to add our label text, which will be text with a value of text.
And then we can use at field error and that will automatically grab the errors from the context defined by our root for the text field that we're defining here. And we aren't going to need to make use of slots for either of these. So we can mark them as either self-closing or add the end tag in to end them out. Finally, then we need to add in our input control. So we can do atinput control. And this input control is going to automatically grab off of the context provided by our root, the name of the field as well as the old value if there is one to use on the input itself.
So all that we need to do here is define the type of the input. And in HTML text is going to be the default type. So we don't necessarily need to provide that here, but we'll go ahead and do it anyway. So this gives us in a nice little package the exact same thing that we have down here. So we can go ahead and get rid of it al together, leaving just the field root. We can go ahead and give this a copy and replace our points markup down here with the same. Switching the name from text to points, the label from text to points, and the type from text to number.
The only thing that these components are using that we haven't already covered are prop helpers, which I actually think we mentioned in passing. So, if we take a look at our field here, we can jump into the root. And you'll notice this is just doing a conditional on whether or not it should be in a group. And then inside of there, it's either updating the context for that group and injecting the context for the child contents inside of it for our label control and error, or it's defining the field key from the name that we've passed into the props of this component and injecting [clears throat] the state in for the child components that we're using.
Again, a label control and error to make use of inside of them. Inside of our label is where we have some of the prop merging going on. So, it's taking the prompts provided to this component, omitting the text because it's going to use that as the label text, but merging in additional things like what field this is for, classes, and whether or not it's invalid. Then, it will convert those to HTML attributes, plopping them into the label element itself. Finally, we have our error, which is going to take the contextual errors that's been provided from the root, and loop them out into a div for our error messages.
If we take a look real quick at the input control, it's going to be relatively similar. So, it's using prop merging again to build out the HTML attributes that should be applied to the input as well as inline variables. All of this we touched on within our Edge.js markup lessons. Just kind of taking it to a bit more of an extreme. All right, so it looks like I didn't save my create page. Let me jump back into there. Give that a save. Uh, and we can actually go ahead and just get rid of our dump there as well.
Uh, we'll give this a save. Jump back into our browser. And we can try this out one more time. So, we'll do test with uh 5,000 points. Hit enter. And voila, we get back the exact same behavior. It's just now it's also adding some additional classes that we didn't previously have to our error messages as well as our invalid fields to mark them as red. Everything else though is behaving the same as we had it. So let's next dive into our edit page. So we can go back dive into a specific lesson, edit it to do similar here because if we provided an invalid input here, it's just going to behave the same as we had at the start of this lesson.
So, we can actually go ahead and just copy these two divs here from our create page, jump into our edit page, and replace them with what we currently have. The only thing that we need to do now is provide in a default value for our inputs into our control components like we had previously on our input elements directly. So, we'll set a value of challenge.ext for our text field and value challenge. Points field. Again, if we dig into and I'm going to commandclick into our input. control component. Right up here is where it's merging in either the old value that it's getting from the root context or making use of the value that we're providing directly via the props of this component.
It then sets that merges it into the HTML attributes of the input right there, giving us the exact same behavior that we saw on our create page, just now with the ability to have a default value defined on our form. Fantastic.
More from Adocasts
Get daily recaps from
Adocasts
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.




