Forms, CSRF and flash messages
Let us build the add-post form in full: a page with the form, protection against forged requests, errors next to the fields and a "Post published" message after saving. AdonisJS provides all of this out of the box in the Web kit.
Routes
// start/routes.ts
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.get('/posts/create', [PostsController, 'create']).as('posts.create')
router.post('/posts', [PostsController, 'store']).as('posts.store')The first route shows the form, the second receives its data.
The form template
{{-- resources/views/pages/posts/create.edge --}}
@layout({ title: 'New post' })
<h1>New post</h1>
<form method="POST" action="{{ route('posts.store') }}">
{{ csrfField() }}
<label>Title
<input type="text" name="title" value="{{ old('title') || '' }}">
</label>
@inputError('title')
@each(message in $messages)
<p class="error">{{ message }}</p>
@end
@end
<label>Text
<textarea name="body">{{ old('body') || '' }}</textarea>
</label>
@inputError('body')
@each(message in $messages)
<p class="error">{{ message }}</p>
@end
@end
<button type="submit">Publish</button>
</form>
@endThe controller's create method simply returns view.render('pages/posts/create'). Open /posts/create — the form appears.
What happens here
| Code | Purpose |
|---|---|
csrfField() | A hidden field with the CSRF token |
old('title') | The value the user typed before the error |
@inputError('title') | The block renders if the field has errors; they are in $messages |
CSRF (Cross-Site Request Forgery) is an attack in which another site submits a form to yours on behalf of a logged-in user. The Shield package (config/shield.ts) rejects every POST without the correct token.
Saving and a flash message
// app/controllers/posts_controller.ts
async store({ request, response, session }: HttpContext) {
const payload = await request.validateUsing(createPostValidator)
const post = await Post.create(payload)
session.flash('success', 'Post published')
return response.redirect().toRoute('posts.show', { id: post.id })
}A flash message is kept in the session only until the next request. Show it in the layout:
{{-- resources/views/components/layout.edge --}}
@flashMessage('success')
<div class="alert alert-success">{{ $message }}</div>
@endSubmit an empty form — it comes back with errors under the fields and the values you typed. Submit a valid one — the post page opens with a green "Post published" banner.
Forms for PUT and DELETE
HTML forms can only do GET and POST. For updating and deleting, the method is passed in the form address as the _method parameter:
{{-- resources/views/pages/posts/show.edge --}}
<form method="POST" action="{{ route('posts.destroy', { id: post.id }) }}?_method=DELETE">
{{ csrfField() }}
<button type="submit">Delete post</button>
</form>This works when config/app.ts has allowMethodSpoofing: true in the http section.
Common mistake
If you forget csrfField(), submitting the form fails with Invalid or expired CSRF token and nothing is saved. Add csrfField() to every form with the POST method, including delete and logout forms.