Forms and Validation
In this chapter, you'll first add the ability for authenticated users to create new posts. Then, you'll apply the same pattern to let users leave comments on existing posts. Along the way, you'll be introduced to AdonisJS's validation layer and learn how to organize your code using separate controllers for different resources.
Overview
So far in the DevShow tutorial, you've built an application that displays posts from your database. But what about creating new posts? That's where forms come in.
Handling forms involves three main steps:
- Displaying a form to collect user input.
- Validating that input on the server to ensure it meets your requirements.
- Finally saving the validated data to your database.
AdonisJS provides Edge form components that render standard HTML form elements with automatic CSRF protection, and VineJS for defining validation rules.
Adding post creation
Let's start by adding the ability for users to create new posts. We'll need a controller method to display the form, routes to wire everything up, and a template for the form itself.
-
Add controller methods
First, let's add a
createmethod to yourPostsControllerthat will render the form for creating a new post. We'll also stub out astoremethod that we'll implement later to handle the form submission.app/controllers/posts_controller.tsimport type { HttpContext } from '@adonisjs/core/http' import Post from '#models/post' export default class PostsController { // ... existing methods (index, show) /** * Display the form for creating a new post */ async create({ view }: HttpContext) { return view.render('posts/create') } /** * Handle the form submission for creating a new post */ async store({}: HttpContext) { // We'll implement this later } } -
Register the routes
Now let's wire up the routes. We need two: one to display the form and another to handle submissions. Both should only be accessible to logged-in users.
start/routes.tsimport router from '@adonisjs/core/services/router' import { middleware } from '#start/kernel' import { controllers } from '#generated/controllers' router.get('/posts', [controllers.Posts, 'index']) router.get('/posts/create', [controllers.Posts, 'create']).use(middleware.auth()) router.post('/posts', [controllers.Posts, 'store']).use(middleware.auth()) router.get('/posts/:id', [controllers.Posts, 'show'])The
auth()middleware ensures only logged-in users can access these routes. Unauthenticated visitors will be redirected to the login page. -
Create the form template
Create the template for the form using the Ace CLI.
node ace make:view posts/createThis creates
resources/views/posts/create.edge. Open it and add the following form.resources/views/posts/create.edge@layout() <div class="form-container"> <div> <h1> Create a new post </h1> </div> <div> @form({ route: 'posts.store', method: 'POST' }) <div> @field.root({ name: 'title' }) @!field.label({ text: 'Post title' }) @!input.control({ placeholder: 'Enter an interesting title' }) @!field.error() @end </div> <div> @field.root({ name: 'url' }) @!field.label({ text: 'URL' }) @!input.control({ type: 'url', placeholder: 'https://example.com/article' }) @!field.error() @end </div> <div> @field.root({ name: 'summary' }) @!field.label({ text: 'Short summary' }) @!textarea.control({ rows: 4, placeholder: 'Describe what this post is about' }) @!field.error() @end </div> <div> @!button({ text: 'Create Post', type: 'submit' }) </div> @end </div> </div> @endThese Edge form components are part of the starter kit. They render standard HTML elements with helpful features like automatic CSRF protection (via
@form) and validation error display (via@!field.error()). -
Create a validator
Before handling form submissions, we need to define validation rules. AdonisJS uses VineJS for validation — a schema-based validation library that lets you define rules for your data.
Create a validator using the Ace CLI.
node ace make:validator postThis creates
app/validators/post.ts. Add acreatePostValidatorto validate post creation.app/validators/post.tsimport vine from '@vinejs/vine' /** * Validates the post's creation form */ export const createPostValidator = vine.compile( vine.object({ title: vine.string().minLength(3).maxLength(255), url: vine.string().url(), summary: vine.string().minLength(80).maxLength(500), }) )The
vine.compile()creates a reusable validator from a schema. Inside, we define each field with its type and rules.- The
titlefield must be string between 3-255 characters. - The
urlfield must be a string and formatted as a URL. - The
summaryfield must be between 80-500 characters.
- The
-
Implement the store method
Now let's implement the
storemethod to validate the data, create the post, and redirect the user.app/controllers/posts_controller.tsimport type { HttpContext } from '@adonisjs/core/http' import Post from '#models/post' import { createPostValidator } from '#validators/post' export default class PostsController { // ... existing methods async store({ request, auth, response }: HttpContext) { const payload = await request.validateUsing(createPostValidator) await Post.create({ ...payload, userId: auth.user!.id, }) return response.redirect().toRoute('posts.index') } }When the form is submitted,
request.validateUsing()validates the data. If validation fails, the user is automatically redirected back with errors that appear next to the relevant fields. If validation succeeds, we create the post and redirect to the posts index.Now visit
/posts/create, fill out the form, and submit it. Your new post should appear on the posts page! Try submitting invalid data (like a short summary or invalid URL) to see the validation errors in action.
Adding comments to posts
Now that you can create posts, let's add the ability for users to leave comments. We'll create a separate controller for comments—having one controller per resource is the recommended approach in AdonisJS.
-
Create the comment validator
Let's start by defining validation rules for comments.
node ace make:validator commentSince comments only have a content field, the validation is simple.
app/validators/comment.tsimport vine from '@vinejs/vine' /** * Validates the comment's creation form */ export const createCommentValidator = vine.compile( vine.object({ content: vine.string().trim().minLength(1), }) ) -
Create the CommentsController
Generate a new controller using the Ace CLI.
node ace make:controller commentsThis creates
app/controllers/comments_controller.ts. Add astoremethod to handle comment submissions.app/controllers/comments_controller.tsimport type { HttpContext } from '@adonisjs/core/http' import Comment from '#models/comment' import { createCommentValidator } from '#validators/comment' export default class CommentsController { /** * Handle the form submission for creating a new comment */ async store({ request, auth, params, response }: HttpContext) { // Validate the comment content const payload = await request.validateUsing(createCommentValidator) // Create the comment and associate it with the post and user await Comment.create({ ...payload, postId: params.id, userId: auth.user!.id, }) // Redirect back to the post page return response.redirect().back() } }We're using
params.idto get the post ID from the route parameter, andresponse.redirect().back()to send the user back to the post page. -
Register the comment route
Add a route for creating comments, also protected by the auth middleware.
start/routes.tsimport router from '@adonisjs/core/services/router' import { middleware } from '#start/kernel' import { controllers } from '#generated/controllers' router.on('/').render('pages/home').as('home') router.get('/posts', [controllers.Posts, 'index']) router.get('/posts/create', [controllers.Posts, 'create']).use(middleware.auth()) router.post('/posts', [controllers.Posts, 'store']).use(middleware.auth()) router.get('/posts/:id', [controllers.Posts, 'show']) router.post('/posts/:id/comments', [controllers.Comments, 'store']).use(middleware.auth())The
:idparameter captures the post ID, which we access asparams.idin the controller. -
Add the comment form
Open your
resources/views/posts/show.edgetemplate and add the comment form.resources/views/posts/show.edge@layout() {{-- ... existing post display code ... --}} <div class="posts-comments"> <h2>Comments</h2> <div class="post-comment-form"> @form({ route: 'comments.store', routeParams: post, method: 'POST' }) <div> @field.root({ name: 'content' }) @!textarea.control({ rows: 3, placeholder: 'Share your thoughts...' }) @!field.error() @end </div> <div> @!button({ text: 'Post comment', type: 'submit' }) </div> @end </div> {{-- ... existing comments list ... --}} </div> @endThe
routeParams: postpasses the post object to the route helper, generating the correct URL like/posts/1/comments.Now visit any post page while logged in and try leaving a comment. After submitting, you'll be redirected back to see your comment in the list.
What you learned
You've now added full form handling and validation to your DevShow application. Here's what you accomplished:
- Created forms using Edge form components (
@form,@field.root,@input.control, etc.) - Defined validation rules using VineJS validators
- Validated form submissions in your controllers using
request.validateUsing() - Protected routes with the
auth()middleware to ensure only logged-in users can create content - Associated posts and comments with users using
auth.user!.id - Organized your code by creating separate controllers for different resources (PostsController and CommentsController)
- Handled form errors automatically with the
@!field.error()component