#13 | Form Basics, Method Spoofing, & CSRF - AdonisJS 7
Chapters12
This chapter explains creating a dedicated create page for challenges and setting it up to post to a store endpoint, following resourceful naming conventions within the challenges folder.
AdonisJS 7 form basics, CSRF tokens, and method spoofing explained through a hands-on build of a Challenge manager with in-memory storage.
Summary
Adocasts breaks down building a form-driven feature in AdonisJS 7, following resourceful routing conventions from creation to editing. The lesson shows creating a create.edge page, wiring routes for GET and POST, and rendering views with a hero layout. It then dives into form handling, illustrating how to pull data from the request using request.all, and why CSRF protection adds a hidden token field via AdonisCSRF. The instructor demonstrates defensive data handling with only and input methods, and explains how to redirect after a successful store. When extending to editing, the video covers preloading defaults into the edit form, route parameters, and HTTP method spoofing to support PUT requests from standard HTML forms. Throughout, the example uses an in-memory challenges array, emphasizing that data won't survive server restarts and highlighting the practical realities of a tutorial vs. a real database. By the end, viewers see how to create, list, edit, and update challenges, plus the role of HTTP verbs, CSRF, and method spoofing in a smooth UX.
Key Takeaways
- Create a dedicated create page at pages/challenges/create and post to the store endpoint slash/challenges, following AdonisJS resourceful routing conventions.
- Use AdonisJS routes to separate concerns: a GET for /challenges/create renders the form, and a POST to /challenges handles submission and storage.
- Enable CSRF protection and insert a CSRF token field into the form using AdonisCSRF helper so the server can validate requests (hidden input with the token).
- Defensively read form data with only(['text','points']) or request.input('field') to avoid accidentally processing disallowed data, while noting validation is coming in a future step.
Who Is This For?
Essential viewing for AdonisJS 7 learners who want to understand form handling, CSRF protection, and method spoofing in practical route-driven apps.
Notable Quotes
"CSRF stands for cross-sight request forgery."
—Explains what CSRF is and why tokens are necessary.
"When we submit a post, put, patch or delete request... it will expect that token to be sent along with the request."
—Describes CSRF token validation in AdonisJS.
"To protect against these malicious attacks, our server is going to generate a token with every request."
—Summarizes the protective mechanism behind CSRF tokens.
"HTTP method spoofing allows us to send our request from our form as a post, but have the server understand it as a put."
—Introduces how forms simulate PUT/PATCH/DELETE methods.
"Responses are how we send instructions and data from our server back to the user inside of the browser."
—Covers how AdonisJS handles responses and redirects.
Questions This Video Answers
- How does AdonisJS 7 implement CSRF protection in forms?
- What is HTTP method spoofing and how do you use it in AdonisJS 7 forms?
- How can I read form data safely in AdonisJS 7 without using request.all?
- What are resourceful routes in AdonisJS and how do GET/POST routes differ for the same resource?
- Why would data stored in an in-memory array disappear after a server restart and how to persist it?
AdonisJS 7Form BasicsCSRFHTTP Method SpoofingRoutingEdge TemplatingIn-Memory Data StoreResourceful RoutingHTTP VerbsRoute Params
Full Transcript
Now that we're listing our challenges, let's next add the ability to create a new challenge by adding in a form. First though, we're going to need to make a page. And if you'll recall back to our resourceful naming conventions, convention states that this should be a create page inside of our challenges folder. And that create page should post up to a store endpoint. So, we'll right click on our challenges folder, or you can use the ACLI for this, and make a new create.edge page inside of there. With that stubbed, we can then jump into our routes.
And if you don't feel like finding it either in your file tree or up inside of your recent files, you can always hit command P. And in Visual Studio Code, this will pull up a list of files within our project that we can search against. And this default list here is going to be a list of our most recently opened. So within here, we can easily search for routes to find the routes.ts file inside of our start directory. Hit enter to open that up. And away we go. So, we want to add an additional route here for our create page.
So, we'll do router.get/challenges/create. Again, keeping with that resourceful naming. And then we'll point it to our challenges controller and use the create method inside of there to handle this route. Then, within that create method, if we jump into our challenges controller, similar to our index method, at this point, all that we need to do is use view and return view. to render our create page at pages/challenges/create. And this particular page is not going to need any render state. So we can leave that exactly as is. Then we can jump into our index page. And directly here on this view.
Call from our index method. You'll notice that this pages challenges index is underlined. What this is noting to us is that we can treat this as an actual anchor link inside of Visual Studio Code by holding command or control, hovering over it, and clicking on it to jump directly into that file. Again, that's the AdonisJS extension pack adding that in. Okay, so let's take our available points up through our H1 and let's wrap this in an additional div. Give it a class of hero, which is one of the default classes that came with the starter kit, and paste those back in.
Then underneath our available points, we'll add in a new anchor link pointing to challenges slashcreate. And we can give this a class of button and the text of create a new challenge. Give that a save. And next, we can focus on filling out our create page. On this page, we're going to want a form with the fields that our challenge requires in order for it to be created. That's our text for the name of the challenge and points for the available points that should be given to the user for completing said challenge. So, as always, we can start this with our layout.
And then in between the start and end tags of that layout, we can add in a div class form container. Again, another class coming from that starter kit. Add in another div for our header section with an H1 of create challenge. And we can optionally give this a paragraph of enter your challenge details. Below underneath this div, we can add in our form with an action pointing to slash challenges, which we haven't yet set up, but this will inevitably be our store endpoint. And we want this to have a method of post. Inside of this form, we want to put each of our fields.
So, we'll do a div there to wrap them with a label of text for our text input. We'll put an input inside of here of type text with a name of text. A lot of text right there. Okay. Then we want another div for our points. Again, we'll put this inside of a label with the text points and an input. We can give this a type of number with the name of points. Finally, underneath our two fields, we can add in another div with a button of type submit and a class of button. And we'll give this the text of create challenge.
So, at this point, we should be able to give that a save, jump back into our browser, and see a new create a new challenge button on our challenge index page. that hero class kind of gave us some spacing between these two sections as well. If we then click into our create a new challenge page, we're taken to a page that looks like this with our create challenge form. What we next need to do is rig up our form so that whenever we click this button to submit it, it actually does something. So, let's go do that next.
Again, we want to jump inside of our routes first to rig up that route since the controller method is already stubbed. And we want this to be a post HTTP verb because we need to be able to send data up with this request. And get requests don't have a body. Post is also the resourceful naming convention to handle creation events. And we'll put this at / challenges again to keep with that resourceful naming convention. Like all the others, we want to reach through to our challenges controller to handle it. And we want to point this to our store method inside of that controller for its creation.
Here you will notice that we have both a get at /challenges and a post at /challenges. When we send our requests up, they have an HTTP verb attached with them designating whether or not they're a get request or a post request. So that designation, despite these two having the same patterns, is how Adonisjs is going to separate these two to discern which one should handle the request in question. Okay, let's give that a save then and jump back into our challenges controller because we next need to fill out our store handler. When we submit our form, the data within it will be sent up within our requests body.
There are numerous ways in Adon.js to get that body data from our request. And the most simple way to get at that data inside of a request body is to use request.all. This will return both the requests body as well as merge in the query string data as well. And a lot of the times whenever we're getting at body data within Adonisjs, it will merge in query string data as well, making these methods useful for numerous contexts. For example, since get requests don't have a body, if they need to send data up, they're frequently going to use query strings to do so, like passing up parameters to perform a searcher filter.
So, our get requests are going to be able to get at that data similar to how we're going to be able to get at our body data, keeping everything consistent between those two ways to send data. So, for right now, let's merely just console log this out so that we can see exactly what it is we're getting with our form. So, we'll give that a save and jump back into our browser. We can now fill out our form. So, I'm just going to put testing and we'll give it 10 points and I'll hit create challenge.
And you will notice that our page refreshed and our form cleared out here. So, let's go ahead and dig back into our terminal to see exactly what we got. And what you'll notice is instead of our console log data, we're going to get a warning about an invalid or expired CSRF token inside of our console. So, what is this warning about? Well, CSRF stands for cross-sight request forgery. And per the Open Worldwide Application Security Project or OASP, cross-sight request forgery is an attack that forces an end user to execute unwanted actions on a web application of which they're currently authenticated.
With a little help of social engineering, such as sending a leak via email or chat, an attacker may trick the user of a web application into executing actions of the attacker's choosing. And this impacts applications using traditional sessions like ours does. To protect against this, our server is going to generate a token with every request. When we submit a post, put patch or delete request from our application. It will expect that token to be sent along with the request as verification that the request originated from our site as a way to protect against these malicious attacks.
This token is referred to as the CSRF token. And Adonis.js provides a utility method we can call to plop a CSRF token field into our form when we send it up. So if we jump back into our create page and I'm going to hit commandclick to jump into it inside of our form element. This is already automatically sending up any input or field-based element that has a name associated with it like our text and points. So all that we need to do is add in this adonjs provided CSRF field helper by reaching through interpolation and calling it as a method.
So we'll do CSRF field just like so. This will add in a hidden input with that CSRF value inside of it for our server to be able to read and verify our requests validity. Perfect. With that saved, let's jump back into our browser and try our form one more time. So for my text, I'm going to put testing and we'll give it 10 points yet again. When we hit create challenge, this time you'll notice that we get a blank page after we hit that submit button. And this is because we're not really sending anything back as a response.
We're going to change that in a moment, but first let's go back into our terminal and check out our log. So this time you'll notice that we actually got our data console logged out. Fear not about the warning right up here still stating invalid or expired CSRF token. That is just from our previous request. So this data here is from our current request. And you'll notice that we have a CSRF value being sent up with our form data in addition to our text and point values as well. Now, it's never a good idea to take in any data that the user sends to us.
Always be defensive whenever it comes to the data that your application is going to accept from users. So, instead of using request all as we are here, we can switch this to only accept properties we explicitly expect to come up with our request. And again, for this, we have a couple of options. So, first let's take a look at a method called only. And this is similar to the all method except it allows us to provide in an array of fields that we only want to pluck out of the body data. So if we just pass in text and points here, give that a save, jump back into our browser, since we're just at / challenges here, we can go into our URL bar, just hit enter to resubmit that as a get request, and jump back into our create a new challenge page.
Let's hit testing one more time, and we can send up again any point value there. Hit create, and again, we're taken back to this blank page. jump back into our browser to take a look at our data. And you notice this time we're only getting back the two properties that we explicitly defined that we wanted our text and points. The hidden CSRF value is no longer being plopped into our data variable and being omitted from it. Alternatively to only we can also get individual properties as well. So, if we wanted to grab our text from our request, there is an input method.
And we just need to provide in the field name as the first argument. And this optionally accepts a default value as the second argument as well. So, if you wanted to, you could say something like default challenge text there. And if we sent up our request with empty text data, this default challenge text would be used by default. I'm going to go ahead and omit that though. And let's also grab our points in a similar fashion. So we'll do request input and points just like so. With this approach, we no longer have a single data variable, but now individual text and points variables.
So if we console log each of those out, we should see we still get a similar shape here. So again, I'm going to hit enter in our URL bar, jump back into our new challenge page, do testing with a point value of 10, hit enter to submit that form, and we can jump back into our terminal now to see still a similar shape here, just now those properties are split. I do want to note though that though the only and input methods are a little bit more defensive than using just request.all, even these aren't ideal because the user can still send up any value that they want.
Instead, what we want to do is always validate that user sent data and we're going to cover that in the next lesson. So, don't brush your hands off here thinking that you're done quite yet. We will alter this to validate it in the next lesson. Now, typically we would want to take in data and store it within a database for long-term storage. At this point, we're not quite there yet. So, we're going to work with what we have at the moment, which is our in-memory challenges array that we have at the top of our controller file.
Note that since this array is in memory, anything that we add or remove to it will be undone whenever we restart our server as that's going to destroy that memory in essence. So, with our text and point values plucked off of our request, all that we need to do to add a challenge to our in-memory data is to push it into that array. So we can grab our challenges array, call the push method, and provide that data in. So we'll add in our text and points properties. Now, TypeScript isn't going to be happy with this because we're also missing an ID property of type number, which uniquely identifies the challenge in our array.
So we do also need to add in an ID to this as well. And for right now, since this is just in memory, all that we can do is plus one on the length of our challenge array. So since we're starting with three, if we push in a new one, it will be given an ID of four. Additionally, so that we aren't met with a blank page again, we can also forward our user back to the challenges index page so that we can see our updated list with our new item added into our challenges array.
Responses are how we send instructions and data from our server back to the user inside of the browser. That's also how we're rendering our pages as well. The view render method renders out the edge.js JS page, but whenever we return it back, Adonisjs will automatically take whatever we return there if it's of a certain type and automatically append it onto the body of our response for us. But we also have the agency to describe how our response should be handled ourselves as well. And to do that, we can pluck the response object off of our HTTP context.
Using this, we can return and then use our response to redirect the user to a different location. This is going to alter our response to a 302 status code with a location header set on it. And those instructions are how the browser will know to change the page for the user. So we can set this to a path of slashchallenges to go back to our challenges index page just like so. Okay, give that a save and we can jump back into our browser now. Again, we'll just hit enter there. Go back into our create new challenge page and we can try actually creating a new challenge at this point in time.
So we'll do testing and we can give it a value of 10. Hit create challenge and we're redirected back to our challenge index page. And we now have a fourth item in our available challenges containing our testing challenge. Furthermore, we can even jump into that page to see its specific show details as well. One thing you might notice whenever we were console logging the values being sent up with our request at the current moment, the point value of 10 that we're submitting whenever we created our testing challenge is going up as a string. And in JavaScript, whenever you try to aggregate a number against a string, it's just going to concatenate the two together.
So we have the 35 from our three previous challenges, which have a point number value being concatenated with the 10 string value of our new testing. We could easily fix that by casting the point value. But we will fix this in the next lesson anyway whenever we implement validation. So with that being a small detail, let's just continue onward by adding the ability to edit our challenge. So, jumping back into our text editor again, we'll follow a similar flow as last time. We'll go back down into our challenges page, rightclick, new file, and we want to add in an edit.edge page.
For the most part, we can go ahead and just copy our create page and paste it into our edit page because they're going to be relatively similar. From a user standpoint, all that we want to do is switch create text instances to update because we're no longer creating, but we're going to be updating a specific challenge. We're no longer going to be posting to just slashchallenges, but we'll now also need to specify an identifier for the particular challenge that we're editing. So, we can use interpolation to drop the challenge ID into there. Meaning that we're going to also need to provide the specific challenge that we're editing into the render state of this page.
Additionally, we don't want our users to have to guess again at what exactly the text is of the challenge they're editing. We want to provide that as a default value in our fields. So for the value of these two inputs, we can provide in our challenge.ext as well as if we jump down to our points, the challenge.points for the default value of those two. We can give that a save and jump back into our routes file now to add the two routes that we're going to need for showing the edit page as well as updating the challenge record.
So we'll do a router.get for the edit page. it slashchallenges slash id as a route parameter slashedit keeping again with that resourceful naming and then we'll do controllers handle this with our challenge controller and its edit method then since we're going to be updating instead of doing a post we can use the more appropriate put method which signals an update event on the resource for its pattern then we can do challenges slash and then just the ID route parameter we'll handle this yet again with our challenges controller And this time we want to use its update method inside of there.
Jumping then into that controller to handle both of those. We can scroll down to our edit and update sections. Similar to our show, we want to be able to find the specific challenge that we are trying to edit. So we'll do const challenge equals challenges.find row and then reach for the row ID where that equals the params ID. And remember we have a global cast on our ID route parameters to cast this to a number allowing us to do a strict equality check here. Once we have that we can pluck our view out of our HTTP context and return view render and reach through to our pages challenges and oh whoops I called that edge.
What did I do? Create a page called edge.edge. I sure did. Okay. We want to call that edit. My apologies. So edit there instead of edge. So the actual file name is edit.edge. edge and then we want to switch this here to edit as well. Okay, just an accidental showcase of why that autocomplete is so nifty allowing me to catch my typo or misspeaking there. Okay, then we also need to pass our challenge into the render state for our edit page so that it can use it inside of its URL for its put submission as well as the default values of those fields.
Next, we can go ahead and take care of our update as well. So we'll want our response so that we can redirect the user yet again. And we can grab our data again. Remember we will want to validate this which we'll cover in the next lesson. But for now we can just grab the text and points from our request body. Then we need to find the specific index of the challenge that we're trying to update inside of our in-memory challenges array. So we'll just do find index reach through to the row row ID and then do a strict equality check against our params ID.
Once we have that, we can do challenges update the specific challenge index that we found. Of course, we would also in a real application, if this were our actual code, want to validate that that index is uh valid index and not negative one, that it was actually able to find something. Uh but for now, we'll keep it simple with our little test example here. Setting the ID to the params ID and merging in the data that we found. You can also set the specific text and points value on that direct object there if you wish as well.
Finally, we will return response and redirect our user back to our slash challenges index page so that we can see exactly how it updated. Finally, within our show page, then we can go ahead and give this a class of hero just so it matches our other page and add in an anchor link inside of this hero with an href pointing to our slashchallenges slashchallenge. ID/edit page to allow the user to jump into our edit form to update this particular challenge. And we'll give it the text of edit challenge just like so. Okay, with all of that saved, let's jump back into our browser.
Then you'll notice that our server restarted at some point during that process and we lost our testing challenge. As is expected, it's in memory. So, we can jump into any of our challenges here. Go into its edit challenge page. And you'll notice that it's started with the default values of the actual challenges current values. We can switch this to whatever we'd like. So, I'm just going to change the point value from 10 to 20. Hit update challenge. And you'll notice that we're met with an error page again from Yaouch stating that it cannot post to slashchallenges slash one.
And that makes sense. We don't have a post httpverbed route at /challenges one. If we jump back into our routes really quick, the route that we have defined there is both a get for our show page as well as a put for our update, which is the route that we are meaning to send that form to. However, on our edit page, our form is sending a post request. Your first thought might be, okay, well, all that we need to do is switch that post to put, but not so fast. So, browsers in the HTML spec only recognize post and get here in terms of the verbs that we need to use.
So, we are actually limited to sending a post request with the traditional sense of an HTML form. Instead, we can use something called HTTP method spoofing that allows us to send our request from our form as a post, but have the server understand it as a put, patch, or delete request instead. Within Adonjs7, at least in the current starter kits, this is enabled by default, but you can always find it within your config directory inside of the app configuration. If we scroll down a little bit, you should find inside of the HTTP section this allow method spoofing.
To enable this, you just want that to be set to true. When that's true, in order to use method spoofing, all that we need to do is add a query string to our posted URL with an underscore method query parameter name and then the verb that we want to use or spoof on the server side. So, we can set this method to put. And with that, we're now using HTTP method spoofing for our form, allowing the browser to send it up as a post, but our server to recognize it as a put. So now if we give this a save, we can jump back into our browser one more time.
Let's go ahead and just reerequest our page. Go back into the edit, make our change one more time. So we'll just switch this from 10 to 20. Update our challenge. And perfect. Everything worked a okay. Our learn Adonisjs challenge is now worth 20 points instead of 10. Again, currently ignoring the concatenation weirdness that we have going on with our aggregation there, which will be fixed whenever we implement our validation.
More from Adocasts
Get daily recaps from
Adocasts
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.


