Walidacja requestów w ExpressJS

W tym wpisie chciałbym przybliżyć Ci niezwykle wygodną bibliotekę walidacji requestów do API napisanych w frameworku Express.js.

JOI, bo tak nazywa się wyżej wspomniana biblioteka, została stworzona przez ekipę odpowiadającą za framework HapiJS, ale można jej też użyć w Express.js, dzięki bibliotece express-validation.

JOI pozwala walidować dane przychodzące w parametrach, nagłówku, body, a nawet plikach cookies. Posiada kilkadziesiąt wbudowanych walidatorów i z każdą wersją ich ilość wzrasta, a jeśli to nam nie wystarcza, możemy też w bardzo prosty sposób dodać własne reguły walidacji.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const Joi = require('joi');

const customJoi = Joi.extend((joi) => ({
    base: joi.number(),
    name: 'number',
    language: {
        round: 'needs to be a nip number',
    },
    rules: [
        {
            name: 'nip',
            validate(params, value, state, options) {

              const reg = /^[0-9]{10}$/;
              if(reg.test(value) == false) {
                return this.createError('number.nip', { v: value }, state, options);
              } else {
                const digits = (""+value).split("");
                var checksum = (6*parseInt(digits[0]) + 5*parseInt(digits[1]) + 7*parseInt(digits[2]) + 2*parseInt(digits[3]) + 3*parseInt(digits[4]) + 4*parseInt(digits[5]) + 5*parseInt(digits[6]) + 6*parseInt(digits[7]) + 7*parseInt(digits[8]))%11;
                
                if (parseInt(digits[9]) !== checksum) {
                  return this.createError('number.nip', { v: value }, state, options);
                }
              }

              return value;
            }
        }
    ]
}));

const schema = customJoi.number().nip();

Poza tym, z pomocą JOI możesz także dokonywać operacji konwersji na danych przychodzących, np. usunąć nadmierne białe znaki:

1
const schema = Joi.string().valid('a').trim();

Bardzo przydatną opcją jest też możliwość definiowania walidacji jednego parametru, na podstawie wartości innego parametru:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const schema = Joi.object().keys({
  country: Joi.bool().required(),
  allCreditCards: Joi.array().when('hasCreditCards', {
    is: true,
    then: Joi.array().items({
      type: Joi
      .string()
      .valid(['Visa', 'Mastercard'])
      .invalid('Discover')
      .required(),
      balance: Joi.number().required(),
      payment: Joi.number().required()
    })
  })
});

Dzięki rozszerzeniu express-validation, walidację możemy podpinać nie tylko pod treść requestu (body), ale też pod parametry, query, nagłówki, a nawet pod pliki cookies.

1
2
3
4
5
6
7
8
9
10
11
const user = {
  query: {
    id: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required(),
  },
  body: {
    email: Joi.string().email({ minDomainAtoms: 2 }).required(),
    firstName: Joi.string().required(),
    lastName: Joi.string().required(),
    password: Joi.string().required(),
  }
};

W poniższych przykładach podziałamy na moim repozytorium REST API dla aplikacji budżetu domowego.

W pierwszej kolejności musimy zainstalować potrzebne rozszerzenia:

1
2
npm i --save express-validation
npm i --save hapijs/joi

Następnie tworzymy plik definiujący reguły walidacji dla routingu w Express.js. Ja stworzyłem katalog src/config/validator i umieściłem w nim pliki z nazwami kontrolerów, które zamierzam walidować.

Oto jeden z plików konfiguracji schematu walidowania. src/config/validator/transaction.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const Joi = require('joi');

const transaction = {
  body: {
    category: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required(),
    account: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required(),
    contractor: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required(),
    income: Joi.number().allow(null),
    expense: Joi.number().allow(null),
    description: Joi.string().allow(null)
  }
};

const create = { ...transaction };

const update = {
  params: {
    id: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required(),
  },
  ...transaction
};

const get = {
  params: {
    id: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required()
  }
};

const remove = {
  params: {
    id: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required()
  }
};

const list = {
  params: {
    category: Joi.string().regex(/^[0-9a-fA-F]{24}$/).optional().allow(null),
    contractor: Joi.string().regex(/^[0-9a-fA-F]{24}$/).optional().allow(null),
    dateFrom: Joi.date().optional(),
    dateTo: Joi.date().optional(),
  }
};

const summary = {
  params: {
    category: Joi.string().regex(/^[0-9a-fA-F]{24}$/).optional().allow(null),
    contractor: Joi.string().regex(/^[0-9a-fA-F]{24}$/).optional().allow(null),
    dateFrom: Joi.date().optional(),
    dateTo: Joi.date().optional(),
  }
};

module.exports = {
  create,
  update,
  get,
  remove,
  list,
  summary
};

W tym konkretnym przypadku jako definicję identyfikatorów, wykorzystałem regex do określenia id z bazy MongoDB. Metoda optional oznacza, że parametr nie musi zostać podany. Metoda allow(null) zezwala na podanie null, np. gdy kontrahent transakcji nie został wybrany.

Teraz wystarczy wykorzystać tak przygotowany plik walidacji w deklaracji wywołań HTTP zadeklarowanych w kontrolerach aplikacji.

src/config/router/transaction.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const { Router } = require('express');
const TransactionController = require('../../controllers/transaction');
const { authJwt } = require('../../services/auth');
const validator = require('express-validation');
const validatorSchema = require('../validator/transaction');

const router = new Router();

// crud operations
router.post("/transaction", authJwt, validator(validatorSchema.create), TransactionController.create);
router.get("/transaction/:id", authJwt, validator(validatorSchema.get), TransactionController.get);
router.put('/transaction/:id', authJwt, validator(validatorSchema.update), TransactionController.update);
router.delete('/transaction/:id', authJwt, validator(validatorSchema.remove), TransactionController.remove);

I to by było na tyle.

W przypadku niespełnienia reguł walidacji, JOI zwróci status 400 odpowiedzi HTTP oraz listę błędów w postaci JSON`a z tablicą błędów, a więc dokładnie to, czego od tego typu narzędzia oczekujemy.

Podsumowanie

Co tu dużo pisać, JOI to na prawdę dobre, sprawnie rozwijane narzędzie do walidacji, którym możesz zweryfikować poprawność requestów lub w bardziej zaawansowanych zastosowaniach - dane wędrujące pomiędzy warstwami aplikacji.

Link do JOI
Link do express-validation
Link do repozytorium, w którym namiętnie wykorzystuję bibliotekę JOI.