Validation

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 the api starter kits.

  • Official AdonisJS packages extend VineJS with custom rules. For example, Lucid contributes unique and exists 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 and configure VineJS using the following command.

See also: VineJS documentation

node ace add vinejs
  1. Installs the @vinejs/vine package using the detected package manager.

  2. 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.

Define routes
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.post('posts', [PostsController, 'store'])
router.put('posts/:id', [PostsController, 'update'])
Create controller
node ace make:controller post store update
Scaffold controller
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.

app/validators/post_validator.ts
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(async (db, value, field) => {
const user = await db
.from('users')
.whereNot('id', field.meta.userId)
.where('email', value)
.first()
return !user
})
})
)

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(async (db, value, field) => {
const user = await db
.from('users')
.whereNot('id', field.meta.userId)
.where('email', value)
.first()
return !user
})
})
)

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.

start/validator.ts
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.

start/validator.ts
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?