Серверная часть
Часть 1
99% ошибок мы показываем пользователю. Обычно мы выводим их в уведомлении или под невалидным полем. Что же тогда получается? Если мы 99% ошибок показываем пользователю, то может быть стоит создать для них единый формат? Как для бэка, так и для фронта?
Если да, то какие поля будут в объекте ошибки и какие значения принимать?
Давайте создадим класс BaseError
и расширим им класс Error. Первое что в ней точно будет так это унаследованное поле message.
Это ошибка для разработчика и логирования! Пользователю мы ее никогда выводить не будем!
Для вывода сообщений пользователю нам понадобится поле _errorCode
.
Пример значений:
- uniqConstraint
- formatConstraint
- connection
Теперь значение этого поля мы сможем смапить с сообщениями на фронте:
- Ошибка уникальности
- Неверный формат
- Не удалось подключиться к базе
{
"_message": "Db error",
"_errorCode": "connection"
"_status": 500
}
Это необходимый минимум
Часть 2
Что если нам надо вывести название поля, в котором произошла ошибка?
Добавим поле _key
.
А какое значение при этом было?
Добавим поле _value
.
Я бы назвал эту ошибку CollectableError
.
Так же добавим необязательное поле errors в BaseError
, так как нам надо куда-то складывать наши CollectableError
ы.
{
"_message": "Validation error",
"_errorCode": "invalid",
"_status": 401,
"_errors": {
"username": {
"_key": "username",
"_value": undefined,
"_errorCode": "validateNotUndefined",
"_message": "required"
},
"password": {
"_key": "password",
"_value": 100,
"_errorCode": "validateString",
"_message": "not string"
}
}
}
Выводим:
- Поле "username" обязательно
- Формат "password" должен быть строкой, а получено число
Видите! Нам теперь message вообще не нужен! Мы теперь с легкостью сами можем сгенерировать нужный нам текст.
Я специально не стал класть CollectableError
в массив, а положил в обьект по ключу дублирующий key
: так намного удобнее получать доступ к ошибке на фронте.
Часть 3
Часто при ошибке валидации нам нужно вывести пользователю значение которое мы ожидаем от него. Это может быть проверка на максимальное/минимальное количество символов или паттерн.
Для этого добавим необязательные поля с абстрактным именем _key2
и _value2
.
В маленьком проекте _key2
скорее всего будет принимать лишь несколько имен: pattern, max, min.
{
"_message": "Validation error",
"_errorCode": "invalid",
"_status": 500,
"_errors": {
"username": {
"_key": "username",
"_value": "nick",
"_key2": "min",
"_value2": 5,
"_errorCode": "validateNotLessThan",
"_message": "too less symbols"
},
"email": {
"_key": "email",
"_value": "nick@com",
"_key2": "pattern",
"_value2": "@.*?.",
"_errorCode": "validateByPattern",
"_message": "does not match the pattern"
}
]
}
Выводим:
- Поле "username" должно содержать как минимум 5 символов, получено 4
- Поле "email" должно соответствовать паттерну: @.*?.
Клиентская часть
А теперь самое интересное.
Видели последние два примера? Что если валидаторы, которые выкидывают эти ошибки перенести на фронт и начать их использовать? Минимум лишних телодвижений! И если вдруг вы забудете добавить валидатор на клиенте и он сработает на сервере, то всё отработает так будто ничего и не случилось, потому что с сервера прилетит такой же errorCode и мапиться он будет так же.
Такой подход позволяет:
- Поддерживать несколько языков (весь текст в ошибках должен быть на английском, мапа с языками хранится на фронте)
- Хранить все сообщения в одном месте
- Не отвлекаться при разработке бизнес логики на выдумывание что написать в сообщении, чтобы было ТОЧНО понятно
- Этих данных более чем достаточно чтобы пользователю ТОЧНО было понятно что произошло
- Всё ваше приложение в курсе формата вашей ошибки
- Простота
Когда же стоит выкидывать Error? Наверное только когда вы не сможете вывести это сообщение пользователю и не сможете обработать ошибку.
Дополнительно
Перехват ошибок
Для перехвата прочих ошибок (базы, языка) нам понадобится написать мидлвар для koa
или фильтр для nest
. В них мы будем конвертировать неугодный нам формат ошибок в наш собственный
Вложенные ошибки
Вложенность ошибок я бы посоветовал делать исходя из формата вложенности вашей библиотеки стэйта формы.
Пример для final-form:
{
"_errors": {
"address": {
"address": {
"postcode": {
"_key": "postcode",
"_value": 446303,
"_errorCode": "validateString"
}
}
}
}
ОГРАНИЧЕНИЯ: если ваше поле будет иметь имя "key", то оно будет конфликтовать с полем ошибки "key", поэтому стоит избегать такого названия или именовать поле в ошибке как "_key".
Временно оставлю ссылочку на то что у меня получилось на сервере https://github.com/savchenko91/auth-server/blob/master/src/utils/errors.ts
Top comments (0)