Skip to content

🟒 Your favourite library for validating incoming data in express.js.

License

Notifications You must be signed in to change notification settings

eddienubes/validness

Repository files navigation

Validness πŸ’¦

water water2

ci-status npm version


🟒 Your favourite library for validating incoming data in express.js.

With the creation of a single class and usage of decorators, we achieve validation of the following content types (body parser required):

  • application/json
  • multipart/form-data (yes, with files and text fields)

Also, this library supports both ESM and CJS versions via dynamic exports in package.json.


βš™οΈ Installation:

  1. It has some important peer dependencies:
  • class-validator

  • class-transformer

  • reflect-metadata

    Optional, for multipart/form-data parsing only (These are truly optional):

  • multer (recommended)

  • formidable (yes, we support an ESM version only)

yarn add validness class-validator class-transformer reflect-metadata
npm install validness class-validator class-transformer reflect-metadata

Please import reflect-metadata in an entry file of your project in order for this package to work

import 'reflect-metadata'
// your code

For typescript decorators usage turn this option on in your tsconfig.json file.

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

✍ Usage:

Body validation

As stated above, we're going to use decorators. Let's create a DTO of commonly expected data:

registration.dto.ts

import { IsEmail, IsPhoneNumber } from "class-validator";

export class RegistrationDto {
  @IsPhoneNumber()
  phone: ValidatedFile;

  @IsEmail()
  email: string;
}

registration.controller.ts

import { Router } from "express";
import { validationBodyPipe, DefaultBodyError } from "validness";
import { RegistrationDto } from './registration.dto.ts';
import { StatusCodes } from "http-status-codes";

const router = Router()

// controller
router.post('/', validationBodyPipe(RegistrationDto), async (req, res, next) => {
  const validatedBody = req.body as RegistrationDto;

  await registerUser(validatedBody);

  res.json({ message: 'SUCCESS' })
});

// Error handling
router.use((err, req, res, next) => {
  // validness throws Default error models if customErrorFactory isn't sepcified.
  // Standard models contain everything you need, like status code and errored fields
  if (err instanceof DefaultBodyError) {
    // implement handling logic
    res.status(StatusCodes.BAD_REQUEST).json({ message: 'ERROR' });
  }
});

Query validation

Let's create a query DTO with a use of decorators. As you might know, query params always contain strings, you can transform them with class-transformer decorators.

get-users-query.dto.ts

import { IsEmail, IsNotEmpty, IsNumberString, IsPhoneNumber } from "class-validator";

export class GetUsersQueryDto {
  @IsString()
  @IsNotEmpty()
  query: string;

  @IsNumberString()
  pageIndex: string;

  @IsNumberString()
  perPage: string;
}

users.controller.ts

import { Router } from "express";
import { GetUsersQueryDto } from './get-users-query.dto.ts';
import { validationQueryPipe, DefaultQueryError } from "validness";
import { StatusCodes } from "http-status-codes";

const router = Router()

// controller
router.get('/', validationQueryPipe(GetUsersQueryDto), async (req, res, next) => {
  const validatedQuery = req.query as GetUsersQueryDto;

  await findUsers(validatedQuery);

  res.json({ message: 'SUCCESS' })
});

// Error handling
router.use((err, req, res, next) => {
  // validness throws Default error models if customErrorFactory isn't sepcified.
  // Standard models contain everything you need, like status code and errored fields
  if (err instanceof DefaultQueryError) {
    // implement handling logic
    res.status(StatusCodes.BAD_REQUEST).json({ message: 'ERROR' });
  }
});

Multipart Form Data validation

Install an underlying driver of your choice:

  • multer (highly recommended)
  • formidable (Has some nuances)
yarn add multer
yarn add -D @types/multer

npm install multer
npm install -D @types/multer

In the same simple way we create a new DTO.

sign-up.dto.ts

import { IsEmail, IsNotEmpty, IsNumberString, IsPhoneNumber, IsString } from "class-validator";
import { ValidatedFile, IsFiles, IsFile } from "validness";

export class SignUpDto {
  // form-data text field
  @IsString()
  @IsNotEmpty()
  firstName: string;

  // form-data text field
  @IsString()
  @IsNotEmpty()
  lastName: string;

  // a single file
  @IsFile({ type: 'image' })
  photo: ValidatedFile

  // a multiple files
  @IsFiles({ type: 'image' })
  documents: ValidatedFile[]
}

auth.controller.ts

import { Router } from "express";
import { SignUpDto } from './sign-up.dto.ts';
import { validationFilePipe, DefaultFileError } from "validness";
import { StatusCodes } from "http-status-codes";

const router = Router()

// controller
router.post('/', validationFilePipe(SignUpDto), async (req, res, next) => {
  const validatedBody = req.body as SignUpDto;

  // We do not use any disk storage engines for multer
  // So, validatedBody.photo.buffer is accessbile and stored in memory
  // Consult API section for more details

  await signUp(validatedBody);

  res.json({ message: 'SUCCESS' })
});

// Error handling
router.use((err, req, res, next) => {
  // validness throws Default error models if customErrorFactory isn't sepcified.
  // Standard models contain everything you need, like status code and errored fields
  if (err instanceof DefaultFileError) {
    // implement handling logic
    res.status(StatusCodes.BAD_REQUEST).json({ message: 'ERROR' });
  }
});

That's it! 😊


πŸ€” Nuances I have been talking about:

  1. Formidable.

IMO this library does not have a convenient API, therefore, some decisions were made while developing mine.

If you decide to change core config of this library (coreConfig property), e.g. set max size for files there and not in the decorator, then the error thrown by formidable will NOT be mapped to DefaultFileError, but rather passed as is.

  1. Some options in configuration might not be complete, such as mimetype or type. If you hardly demand new ones, just open an issue and I will expend the list within a day. Same algorithm applies to any improvements you consider essential.

πŸ› οΈ Configuration

Each validation pipe has a couple of extra arguments to customise its or underlying libraries' behaviour. Let's take a look at the validationFilePipe and validationBodyPipe signatures.

// ...
export const validationFilePipe =
  (DtoConstructor: ClassConstructor, config?: ValidationFileConfig): Router => {
  }
// ...
export const validationBodyPipe =
  (DtoConstructor: ClassConstructor, config?: ValidationBodyConfig): RequestHandler => {
  }
// ...

If you want to customise config globally for all pipes, use validness function and pass an object with options there. (Be careful with what you change in coreConfig)

import { validness } from "validness";

validness({
  // And many other options..
  customErrorFactory: errorFactory,
  queryValidationConfig: {
    enableDebugMessages: true
  }
});

πŸš€ API

ValidationConfig

Defaults can be found HERE

I think overall documentation for each property is not required because they're well commented in the code itself.

If you feel a lack of examples - open an issue, I will add as many as you want. Thanks.


❀️ Contributing

If you wish to contribute to evolving of this package, please submit your issues or even open pull requests. You're always welcome. πŸ₯°


License

MIT (c) Sole Cold