Validation
The data validation in AdonisJS is usually performed at the controller level. This ensures you validate the user input as soon as your application handles the request and send errors in the response that can be displayed next to the form fields.
Once the validation is completed, you can use the trusted data to perform the rest of the operations, like database queries, scheduling queue jobs, sending emails, etc.
Choosing the validation library
The AdonisJS core team has created a framework agnostic data validation library called VineJS. Following are some of the reasons for using VineJS.
-
It is one of the fastest validation libraries in the Node.js ecosystem.
-
Provides static type safety alongside the runtime validations.
-
It comes pre-configured with the
web
and theapi
starter kits. -
Official AdonisJS packages extend VineJS with custom rules. For example, Lucid contributes
unique
andexists
rules to VineJS.
However, AdonisJS does not technically force you to use VineJS. You can use any validation library that fits great for you or your team. Just uninstall the @vinejs/vine
package and install the package you want to use.
Configuring VineJS
Install VineJS from the npm packages registry using one of the following commands.
See also: VineJS documentation
npm i @vinejs/vine
yarn add @vinejs/vine
pnpm add @vinejs/vine
Once done, you must run the following command to configure VineJS within an AdonisJS application.
node ace configure vinejs
-
Registers the following service provider inside the
adonisrc.ts
file.{providers: [// ...other providers() => import('@adonisjs/core/providers/vinejs_provider')]}
Using validators
VineJS uses the concept of validators. You create one validator for each action your application can perform. For example: Define a validator for creating a new post, another for updating the post, and maybe a validator for deleting a post.
We will use a blog as an example and define validators to create/update a post. Let's start by registering a couple of routes and the PostsController
.
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.post('posts', [PostsController, 'store'])
router.put('posts/:id', [PostsController, 'update'])
node ace make:controller post store update
import { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async store({}: HttpContext) {}
async update({}: HttpContext) {}
}
Creating validators
Once you have created the PostsController
and defined the routes, you may use the following ace command to create a validator.
See also: Make validator command
node ace make:validator post
The validators are created inside the app/validators
directory. The validator file is empty by default, and you can use it to export multiple validators from it. Each validator is a const
variable holding the result of vine.compile
method.
In the following example, we define createPostValidator
and updatePostValidator
. Both validators have a slight variation in their schemas. During creation, we allow the user to provide a custom slug for the post, whereas we disallow updating it.
Do not worry too much about the duplication within the validator schemas. We recommend you opt for easy-to-understand schemas vs. avoiding duplication at all costs. The wet codebase analogy might help you embrace duplication.
import vine from '@vinejs/vine'
/**
* Validates the post's creation action
*/
export const createPostValidator = vine.compile(
vine.object({
title: vine.string().trim().minLength(6),
slug: vine.string().trim(),
description: vine.string().trim().escape()
})
)
/**
* Validates the post's update action
*/
export const updatePostValidator = vine.compile(
vine.object({
title: vine.string().trim().minLength(6),
description: vine.string().trim().escape()
})
)
Using validators inside controllers
Let's go back to the PostsController
and use the validators to validate the request body. You can access the request body using the request.all()
method.
import { HttpContext } from '@adonisjs/core/http'
import {
createPostValidator,
updatePostValidator
} from '#validators/post_validator'
export default class PostsController {
async store({ request }: HttpContext) {
const data = request.all()
const payload = await createPostValidator.validate(data)
return payload
}
async update({ request }: HttpContext) {
const data = request.all()
const payload = await updatePostValidator.validate(data)
return payload
}
}
That is all! Validating the user input is two lines of code inside your controllers. The validated output has static-type information inferred from the schema.
Also, you do not have to wrap the validate
method call inside a try/catch
. Because in the case of an error, AdonisJS will automatically convert the error to an HTTP response.
Error handling
The HttpExceptionHandler will convert the validation errors to an HTTP response automatically. The exception handler uses content negotiation and returns a response based upon the Accept header value.
You might want to peek through the ExceptionHandler codebase and see how the validation exceptions are converted to an HTTP response.
Also, the session middleware overwrites the renderValidationErrorAsHTML
method and uses flash messages to share the validation errors with the form.
-
HTTP requests with
Accept=application/json
header will receive an array of error messages created using the SimpleErrorReporter. -
HTTP requests with
Accept=application/vnd.api+json
header will receive an array of error messages formatted as per the JSON API spec. -
Server rendered forms using the session package will receive the errors via session flash messages.
-
All other requests will receive errors back as plain text.
The request.validateUsing method
The recommended way to perform validations inside controllers is to use the request.validateUsing
method. When using request.validateUsing
method, you do not have do define the validation data explicitly; the request body and files are passed as data to the validator.
import { HttpContext } from '@adonisjs/core/http'
import {
createPostValidator,
updatePostValidator
} from '#validators/posts_validator'
export default class PostsController {
async store({ request }: HttpContext) {
const data = request.all()
const payload = await createPostValidator.validate(data)
const payload = await request.validateUsing(createPostValidator)
}
async update({ request }: HttpContext) {
const data = request.all()
const payload = await updatePostValidator.validate(data)
const payload = await request.validateUsing(updatePostValidator)
}
}
Validating cookies, headers and route params
When using the request.validateUsing
method you can validate cookies, headers and route params as follows.
const validator = vine.compile(
vine.object({
// Fields in request body
username: vine.string(),
password: vine.string(),
// Validate cookies
cookies: vine.object({
}),
// Validate headers
headers: vine.object({
}),
// Validate route params
params: vine.object({
}),
})
)
await request.validateUsing(validator)
Passing metadata to validators
Since validators are defined outside the request lifecycle, they do not have direct access to the request data. This is usually good because it makes validators reusable outside an HTTP request lifecycle.
However, if a validator needs access to some runtime data, you must pass it as metadata during the validate
method call.
Let's take an example of the unique
validation rule. We want to ensure the user email is unique in the database but skip the row for the currently logged-in user.
export const updateUserValidator = vine
.compile(
vine.object({
email: vine.string().unique((query, field) => {
query.whereNot(
'id',
field.meta.userId
)
})
})
)
In the above example, we access the currently logged-in user via the meta.userId
property. Let's see how we can pass the userId
during an HTTP request.
async update({ request, auth }: HttpContext) {
await request.validateUsing(
updateUserValidator,
{
meta: {
userId: auth.user!.id
}
}
)
}
Making metadata type-safe
In the previous example, we must remember to pass the meta.userId
during the validation. It would be great if we could make TypeScript remind us of the same.
In the following example, we use the vine.withMetaData
function to define the static type of the metadata we expect to use in our schema.
export const updateUserValidator = vine
.withMetaData<{ userId: number }>()
.compile(
vine.object({
email: vine.string().unique((query, field) => {
query.whereNot(
'id',
field.meta.userId
)
})
})
)
Do note, VineJS does not validate the metadata at runtime. However, if you want to do that, you can pass a callback to the withMetaData
method and perform the validation manually.
vine.withMetaData<{ userId: number }>((meta) => {
// validate metadata
})
Configuring VineJS
You may create a preload file inside the start
directory to configure VineJS with custom error messages or use a custom error reporter.
node ace make:preload validator
In the following example, we define custom error messages.
import vine, { SimpleMessagesProvider } from '@vinejs/vine'
vine.messagesProvider = new SimpleMessagesProvider({
// Applicable for all fields
'required': 'The {{ field }} field is required',
'string': 'The value of {{ field }} field must be a string',
'email': 'The value is not a valid email address',
// Error message for the username field
'username.required': 'Please choose a username for your account',
})
In the following example, we register a custom error reporter.
import vine, { SimpleMessagesProvider } from '@vinejs/vine'
import { JSONAPIErrorReporter } from '../app/validation_reporters.js'
vine.errorReporter = () => new JSONAPIErrorReporter()
Rules contributed by AdonisJS
Following is the list of VineJS rules contributed by AdonisJS.
- The
vine.file
schema type is added by the AdonisJS core package.
What's next?
- Learn more about using custom messages in VineJS.
- Learn more about using error reporters in VineJS.
- Read the VineJS schema API documentation.
- Use i18n translations to define validation error messages.