Validation

Request Validation

Vovk.ts offers API that allows to validate request body and query string on back-end and performs zero-cost validation on client-side before request to the server is made.

vovk-zod

vovk-zod (opens in a new tab) is the library that implements Zod (opens in a new tab) validation. It performs validation on the Controller side with ZodModel.parse, converts the Zod object to a JSON Schema (opens in a new tab) that's stored at .vovk.json file, and runs validation on client before the HTTP request is made with Ajv (opens in a new tab).

/src/modules/user/UserController.ts
import { z } from 'zod';
// ... other imports ...
 
const UpdateUserModel = z.object({ name: z.string(), email: z.email() }).strict();
const UpdateUserQueryModel = z.object({ id: z.uuid() }).strict();
 
export default class UserController {
    @put.auto()
    @vovkZod(UpdateUserModel, UpdateUserQueryModel)
    static updateUser(
        req: VovkRequest<z.infer<typeof UpdateUserModel>, z.infer<typeof UpdateUserQueryModel>>
    ) {
        const { name, email } = await req.json();
        const id = req.nextUrl.searchParams.get('id');
 
        return UserService.updateUser(id, { name, email });
    }
}

Creating a Custom Validation Library

You can create a decorator that, first of all, validates request on the server-side and optionally populates controller metadata with validation information that is going to be used by the client.

The simplest example of the validation would be equality validation. It does nothing than checking if received query and body are equal to some definite object but has no practical use outside of this documentation.

At the example below validateEquality decorator is created with createDecorator that accepts 2 arguments: server validation function and init function that populates clientValidators object to define how validation information should be stored at .vovk.json file.

/src/decorators/validateEquality.ts
import { isEqual } from 'lodash';
import { 
  HttpException, HttpStatus, createDecorator, type VovkRequest, type VovkClientOptions 
} from 'vovk';
 
type BodyValidate = Record<string, unknown> | null;
type QueryValidate = Record<string, string> | null;
 
const validateEquality = createDecorator(
  async (req: VovkRequest<unknown>, next, bodyValidate?: BodyValidate, queryValidate?: QueryValidate) => {
    if (bodyValidate) {
      const body = await req.json();
 
      // override req.json to make it to be called again by controller code
      req.json = () => Promise.resolve(body);
 
      if (!isEqual(body, bodyValidate)) {
        throw new HttpException(HttpStatus.BAD_REQUEST, 'Server exception. Invalid body');
      }
    }
 
    if (queryValidate) {
      const query = Object.fromEntries(req.nextUrl.searchParams.entries());
 
      if (!isEqual(query, queryValidate)) {
        throw new HttpException(HttpStatus.BAD_REQUEST, 'Server exception. Invalid query');
      }
    }
 
    return next();
  },
  (bodyValidate?: BodyValidate, queryValidate?: QueryValidate) => ({
    clientValidators: {
      body: bodyValidate,
      query: queryValidate,
    },
  })
);
 
export default validateEquality;

Then create a file that defines client-side validation function as default export.

/src/decorators/validateEqualityOnClient.ts
import { type VovkClientOptions, HttpException, HttpStatus } from 'vovk';
import { isEqual } from 'lodash';
 
const validateEqualityOnClient: VovkClientOptions['validateOnClient'] = (input, validators) => {
  if (validators.body) {
    if (!isEqual(input.body, validators.body)) {
      throw new HttpException(HttpStatus.NULL, `Client exception. Invalid body`);
    }
  }
 
  if (validators.query) {
    if (!isEqual(input.query, validators.query)) {
      throw new HttpException(HttpStatus.NULL, `Client exception. Invalid query`);
    }
  }
};
 
export default validateEqualityOnClient;

At this example validateEquality is used as a controller decorator and validateEqualityOnClient is used internally by the client. Also notice that validateEqualityOnClient throws HttpException with status 0 to simulate regular HTTP exceptions that can be caught by the client-side code.

Here is how the newly created decorator is used at the controller.

/src/modules/hello/HelloController.ts
import type { VovkRequest } from 'vovk';
import validateEquality from '../decorators/validateEquality';
 
export default class HelloController {
    @post.auto()
    @validateEquality({ foo: 42 }, { bar: 'hello' })
    static validatedRequest(req: VovkRequest<{ foo: 42 }, { bar: 'hello' }>) {
        // ...
    }
}

In order to enable client-side validation you need to define validateOnClient option in vovk.config.js file. For more info see customization documentation.

/vovk.config.js
/** @type {import('vovk').VovkConfig} */
const vovkConfig = {
    validateOnClient: './src/decorators/validateEqualityOnClient',
}
 
module.exports = vovkConfig;

If your validation library is published on NPM it needs to follow the same approach but use module name instead of local path to the file.

/vovk.config.js
/** @type {import('vovk').VovkConfig} */
const vovkConfig = {
    validateOnClient: 'my-validation-library/validateEqualityOnClient',
}
 
module.exports = vovkConfig;

Resulting client code is going to look like that:

import { HelloController } from 'vovk-client';
 
// ...
 
const result = await HelloController.validatedRequest({
    body: { foo: 42 },
    query: { bar: 'hello' },
});

validateEqualityOnClient is going to be invoked on every request before data is sent to the server.

Disable client validation

You can set disableClientValidation option mentioned above to true to disable client validation for debugging purposes.

const result = await HelloController.validatedRequest({
    body: { foo: 42 },
    query: { bar: 'hello' },
    disableClientValidation: true,
});

If you want to disable it completely and remove it from .vovk.json file (in case if you want to hide server-side validation implementation) you can use exposeValidation option set to false at the Next.js wildcard router level.

/src/app/api/[[...vovk]]/route.ts
// ...
export const { GET, POST, PATCH, PUT } = initVovk({
    controllers,
    workers,
    exposeValidation: false // don't populate metadata file with validation information
});