惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
S
Securelist
P
Proofpoint News Feed
H
Help Net Security
S
Schneier on Security
T
Tenable Blog
C
Cisco Blogs
S
Security @ Cisco Blogs
博客园 - 司徒正美
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
Google DeepMind News
Google DeepMind News
C
Cybersecurity and Infrastructure Security Agency CISA
Google Online Security Blog
Google Online Security Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News: Ask HN
Hacker News: Ask HN
NISL@THU
NISL@THU
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
T
The Blog of Author Tim Ferriss
aimingoo的专栏
aimingoo的专栏
W
WeLiveSecurity
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Jina AI
Jina AI
腾讯CDC
WordPress大学
WordPress大学
Simon Willison's Weblog
Simon Willison's Weblog
Vercel News
Vercel News
小众软件
小众软件
N
Netflix TechBlog - Medium
有赞技术团队
有赞技术团队
AWS News Blog
AWS News Blog
雷峰网
雷峰网
Forbes - Security
Forbes - Security
The Hacker News
The Hacker News
博客园 - 聂微东
F
Full Disclosure
量子位
Scott Helme
Scott Helme
宝玉的分享
宝玉的分享
A
About on SuperTechFans
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Schneier on Security
Schneier on Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
K
Kaspersky official blog
AI
AI
SecWiki News
SecWiki News
Webroot Blog
Webroot Blog
Martin Fowler
Martin Fowler

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Error 500? Как вместо кодов получать подробные сведения обо всех ошибках сразу
fonkost (Кри · 2026-04-23 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение12 мин

Охват и читатели384

Мнение

Привет, Хабр! Меня зовут Артём Корсаков. Я пишу на Scala и руковожу группой разработчиков в компании «Криптонит», а также веду Scalabook — русскоязычную базу знаний по Scala и функциональному программированию. В этой статье расскажу про обработку ошибок в библиотеке http4s на Scala 3. Мы разберём, как настроить декодирование запросов так, чтобы клиент получал не просто код «500» или «422» с общим сообщением, а сразу видел развёрнутый список всех проблем в запросе. Например, что логин уже занят, пароль содержит недопустимые символы, а капча не введена.

Пожалуй, самая раздражающая ошибка — это получение кода «500» в ответ на запрос, который ты десять раз перепроверил, сверился с документацией и уверен на все 100%, что запрос рабочий. Даже на 110%! 

В такие моменты раздражённо думаешь: «Что же этому серверу надо? Я же чётко сформулировал запрос!».

Ответить на этот вопрос порой сложно. Например, я хочу зарегистрироваться на сайте, ввожу логин/пароль и получаю сообщение «Internal Server Error». Первое желание — тут же покинуть сайт и поискать более дружелюбный.

Давайте подумаем, как можно сделать сообщение об ошибке более информативным. Для этого будем использовать Scala 3, уточняющие типы и http4s.

Представим, что мы создаём API сервиса авторизации, который (помимо прочего) должен регистрировать новых пользователей.

Для начала определим структуру данных для создания нового пользователя.

package ru.scalabook.http4s.model

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import ru.scalabook.http4s.model.LoginRequest.*

case class LoginRequest(
    username: UserName,
    password: Password
)

object LoginRequest:
  type UserName = UserName.T
  object UserName
      extends RefinedType[String, MinLength[2] & MaxLength[20]]

  type Password = Password.T
  object Password
      extends RefinedType[String, MinLength[8] & MaxLength[100]]

Ссылка на код

Одним из преимуществ функционального программирования является то, что мы доверяем полученному результату, так как он детерминирован и не изменяет внешние данные. В данном случае мы утверждаем, что тип UserName — это строка, содержащая от 2 до 20 символов. Мы можем быть уверены, что никто не засунет сюда null или все символы латинского алфавита. Мы можем продолжить уточнять правила типа, например, добавив регулярное выражение, но пока остановимся на более простом определении.

Наша задача — чётко сформулировать предметную область на уровне типов, тем самым избегая целый пласт ошибок во главе с NullPointerException.

Что произойдёт, если мы попытаемся декодировать JSON из запроса в экземпляр LoginRequest?

Для использования автоматического декодировщика добавим импорты circe и поддержку circe от библиотеки уточняющих типов

import io.circe.*  
import io.circe.generic.auto.*  
import io.circe.parser.*  
import io.github.iltotore.iron.circe.given  
import ru.scalabook.http4s.model.LoginRequest.*

Эти библиотеки и соответствующие импорты позволяют нам декодировать JSON и увидеть результаты.

{  
    "username": "admin",    
    "password": "password123"
}

успешно декодируется (decode[LoginRequest](json)) в 

Right(
  LoginRequest(UserName("admin"), Password("password123"))
)

Даже когда входные данные невалидны, мы получаем структурированное и информативное описание ошибки:

{  
    "username": "a",    
    "password": "password123"
}
Left(
  "DecodingFailure at .username: Should have a minimum length of 2 & Should have a maximum length of 20"
)

Пока всё идет по плану! 

Мы способны открыто рассказать пользователю, что не так в его данных. Да, мы могли бы принять тип данных String и затем вручную провалидировать входящий запрос, но String пошёл бы дальше распространяться по коду, что могло бы стать источником не одной ошибки. Например, когда мы бы случайно перепутали бы логин с паролем. Строгая типизация помогает нам сформулировать один раз правила для значений поля. Мы моделируем предметную область на уровне типов: определяем UserName и Password как уточняющие типы со встроенными ограничениями. Это делает невалидные состояния непредставимыми и переносит проверки на этап компиляции (или, как в нашем случае, на этап декодирования).

Возвращаясь к декодированию, мы получаем следующую проблему, когда у нас «ошибка» в более чем одном поле. Например, в логине и пароле.

Пользователя может раздражать, когда его просят исправлять ошибки по одной:

— Логин должен быть от 2 до 20 символов!

— А в пароле нельзя использовать пробелы!

— Смотри, а вот тут ты в возрасте опечатался!

Это продирание сквозь чащу вызывает только раздражение и вопль: «А что ж ты мне сразу не сказала обо всех ошибках во всех полях?!».

Это похоже на общение с бюрократическим госучереждением: 

— Семёнов и Семенов — это разные фамилии!

— А тут квитанцию забыли приложить!

— Вы не там подпись в бланке поставили, заполняйте заново!

И если от госучереждения не избавиться (ёжики плакали, кололись, но продолжали есть кактус), то в случае с API/UX пользователи вполне могут найти более дружелюбный сервис.

Когда мы получаем от пользователя запрос, то желательно сразу рассказывать обо всех ошибках в нём. Так мы дадим возможность исправить их сразу, не заставляя повторять запросы снова и снова.

А сейчас мы имеем следующую проблему:

Декодирование ошибочных полей:

{  
    "username": "abcdefghijklmnopqrstuvwxyz",    
    "password": "pass"
}

сообщает нам только о первой ошибке:

Left(
  "DecodingFailure at .username: Should have a minimum length of 2 & Should have a maximum length of 20"
)

Править первую ошибку для того, чтобы затем получить следующую — неприятная картина.

Давайте используем другой метод — decodeAccumulating:

decodeAccumulating[LoginRequest](json)

Он выдаёт нам все ошибки за раз:

Validated.Invalid(
  NonEmptyList(
    "DecodingFailure at .username: Should have a minimum length of 2 & Should have a maximum length of 20", 
    "DecodingFailure at .password: Should have a minimum length of 8 & Should have a maximum length of 100"
  )
)

Прекрасно — это то, что нужно! Сейчас мы хотя бы говорим: «Дружок, ты всё классно сделал, но есть пара недочётов. Пришли нам исправленный запрос и мы распахнём перед тобой парадные двери нашего сервиса. Ну чуть-чуть же осталось, всего две ошибочки! Давай, ты сможешь!»

Пойдём теперь писать сервис.

Напишем простой сервис взаимодействия с пользователями:

trait UserService[F[_]]:
  def create(username: UserName, password: Password): F[UserId]
  def get(id: UserId): F[Option[User]]

где User — это тот же LoginRequest, только с идентификатором:

case class User(  
    id: UUID,  
    username: UserName,  
    password: Password  
)

object User:
  type UserId = UserId.T
  object UserId extends RefinedType[Int, Positive]

Создадим сервис:

object UserService:  
  def make[F[_]: {MonadThrow, Sync}](): Resource[F, UserService[F]] =  
    Resource.eval:  
      Ref[F].of(Map.empty[UserId, User])  
        .map(state => new UserService[F](state)) 

Ссылка на код

Мы здесь создаём пользователя по логину/паролю, возвращая его уникальный идентификатор. Сообщаем, что логин занят, если это так, а также возвращаем пользователя по идентификатору, если таковой найден. Всё как и обещалось в интерфейсе UserService: только методы create и get.

Затем создаём роуты, как советует документация:

case class UserRoutes[F[_]: {JsonDecoder, MonadThrow, Concurrent}](  
    service: UserService[F]  
) extends Http4sDsl[F]:  
  given entityDecoder[A: Decoder]: EntityDecoder[F, A] = 
    jsonOf[F, A]  

  private[routes] val prefixPath = "/auth"  

  private val httpRoutes: HttpRoutes[F] =  
    HttpRoutes.of[F]:  
      case GET -> Root / UserIdVar(id) =>  
        Ok(service.get(id))  

      case req @ POST -> Root =>  
        for  
          newUser <- req.as[LoginRequest]  
          id      <- service.create(newUser.username, newUser.password)  
          resp    <- Created(id)  
        yield resp  

  val routes: HttpRoutes[F] = Router(prefixPath -> httpRoutes)  

object UserIdVar:
  def unapply(str: String): Option[UserId] =
    for
      int <- str.toIntOption
      id  <- UserId.option(int)
    yield id

Да, слишком много кода на квадратный сантиметр статьи, но мы просто идём по документации. Осталось совсем чуть-чуть и сервис будет готов!

Давайте развернём сервис и посмотрим, что получилось:

object Main extends IOApp:  
  private val userRoutesRes: Resource[IO, UserRoutes[IO]] =  
    UserService.make[IO]().map: service =>  
      UserRoutes[IO](service)  

  def run(args: List[String]): IO[ExitCode] =  
    userRoutesRes.use: routes =>  
      EmberServerBuilder  
        .default[IO]  
        .withHost(ipv4"0.0.0.0")  
        .withPort(port"8080")  
        .withHttpApp(routes.routes.orNotFound)  
        .build  
        .use(_ => IO.never)  
        .as(ExitCode.Success)

Сервис запущен и теперь можно посылать туда запросы.

Очень-очень кратко о том, что происходит:

  1. Мы развернули на http://localhost:8080 сервис, куда по URL http://localhost:8080/auth можно ходить методами POST и GET с запросом.

  2. Запросы в любое другое место будут возвращать "400 Bad Request" — спасибо методу .orNotFound

  3. Внутри запросов мы дёргаем service.create и service.get 

Давайте уже пошлём какой-нибудь запрос!

Корректный запрос на создание пользователя отрабатывает великолепно:  

curl --request POST \
  --url http://localhost:8080/auth \
  --header 'Content-Type: application/json' \
  --data '{
    "username": "admin",
    "password": "password"
}' 

 возвращает ответ "201 Created:"

"808eee85-7e0e-446a-aa9e-ea84eef238ed" 

Великолепно: захотели создать пользователя — создали его! 

Мы даже можем запросить самого пользователя:

curl --request GET \
  --url http://localhost:8080/auth/808eee85-7e0e-446a-aa9e-ea84eef238ed \
  --header 'Content-Type: application/json'

И получим ответ "200 OK" с телом:

{
  "id": "808eee85-7e0e-446a-aa9e-ea84eef238ed",
  "username": "admin",
  "password": "password"
} 

Пока всё идёт нормально. Давайте теперь пошлем POST-запрос с теми же параметрами, что и в прошлый раз:  

Ответ: "500 Internal Server Error". Ба-бах! Плохо, что-то пошло не так... Откуда-то взялось исключение и самый распространённый ответ пользователю: «что-то упало, но не знаю что...».

Я начал с того, что мы должны доверять написанному коду, но код доверия не заслуживает. Во всяком случае, на данный момент!

Как понять, что роуты удовлетворяют нужному поведению?

Давайте посмотрим на запуск создания пользователя:

for  
  newUser <- req.as[LoginRequest]  
  id      <- service.create(newUser.username, newUser.password)  
  resp    <- Created(id)  
yield resp

Это запрос, завёрнутый в эффект:

  • мы парсим входящий запрос;

  • пытаемся создать пользователя;

  • формируем ответ.

Запрос может пойти не так в двух случаях:

  1. Когда не удалось декодировать входящий запрос

  2. Когда не удалось создать пользователя

Попробуем преобразовать «что-то пошло не так» в BadRequest:

val run = 
  for  
    newUser <- req.as[LoginRequest]  
    id      <- service.create(newUser.username, newUser.password)  
    resp    <- Created(id)  
  yield resp 

run  
  .recoverWith:  
    case ex =>  
      BadRequest(ex.getMessage)

Теперь при запросе на занятый логин получаем ответ: "400 Bad Request"  — то, что нужно:

"Пользователь с таким именем уже существует!"  

Причём исключения http4s можно сразу преобразовать в запрос:

case ex: MessageFailure =>  
  ex.toHttpResponse(HttpVersion.HTTP/1.1).pure[F]  
case ex =>  
  BadRequest(ex.getMessage)

И тогда при ответе, например, на пустой запрос, получим:

curl --request POST \
  --url http://localhost:8080/auth

  400 Bad Request:

The request body was malformed.

Уже лучше! Хотя бы от «500» избавились!

Но проблема всё же остаётся.

При ошибках в самом запросе получаем неинформативное сообщение:

{
    "username": "abcdefghijklmnopqrstuvwxyz",
    "password": "pass"
}

Ответ: "422 Unprocessable Content"

The request body was invalid.

Что значит «с телом запроса что-то не так?!». Это опять же, не юзер-френдли сообщение. И не так уж далеко мы ушли от 500-ки, только код поменяли.

Что не так с моим логином и паролем?! Можно мне всё-таки получить нормальное сообщение от сервера, которое бы ясно дало понять, где я ошибся?! Я же не с Шелдоном из «Теории большого взрыва» общаюсь!

Во-первых, давайте исправим декодер и продолжим собирать все ошибки за раз:

jsonOf[F, A] из документации даст нам только первую ошибку декодирования и вернёт изначальную проблему, accumulatingJsonOf же выдаст нам их все.

EntityDecoder выдаёт нам InvalidMessageBodyFailure(details: String, cause: Option[Throwable]), где в details, к сожалению, лежит малопонятное «тело запроса плохое», а вот в cause уже скапливаются нужные нам ошибки декодирования.

Давайте добавим ещё одно условие в recoverWith:

case ex: InvalidMessageBodyFailure =>  
  Status.UnprocessableContent(  
    ex.cause.map(_.getMessage).getOrElse(ex.getMessage)  
  )  
case ex: MessageFailure =>  
  ex.toHttpResponse(HttpVersion.HTTP/1.1).pure[F]  
case ex =>  
  Status.BadRequest(ex.getMessage)

Если нам удалось собрать ошибки декодирования, то именно их мы и пошлём пользователю.

И вот только теперь мы действительно рассказываем пользователю, что произошло:

Запрос с некорректными типами полей:

curl --request POST \
  --url http://localhost:8080/auth \
  --header 'Content-Type: application/json' \
  --data '{
    "username": null,
    "password": null
}'

выдаёт:

"DecodingFailure at .username: Got value 'null' with wrong type, expecting string\nDecodingFailure at .password: Got value 'null' with wrong type, expecting string"

Запрос с невалидными значениями:

curl --request POST \
  --url http://localhost:8080/auth \
  --header 'Content-Type: application/json' \
  --data '{
    "username": "abcdefghijklmnopqrstuvwxyz",
    "password": "pass"
}'

выдает:

"DecodingFailure at .username: Should have a minimum length of 2 & Should have a maximum length of 20\nDecodingFailure at .password: Should have a minimum length of 8 & Should have a maximum length of 100"

А если послать совсем не те поля:

curl --request POST \
  --url http://localhost:8080/auth \
  --header 'Content-Type: application/json' \
  --data '{
    "field": "admin"
}'

то получим:

"DecodingFailure at .username: Missing required field\nDecodingFailure at .password: Missing required field"

Это уже похоже на правду и на диалог с пользователем вместо того, чтобы отмахиваться от него грязными тряпками в виде «пятисоток».

Мы даже можем пойти дальше и описать, что именно требуется от пользователя:

type UserName = UserName.T  

object UserName  
    extends RefinedType[String, DescribedAs[  
      MinLength[2] & MaxLength[20],  
      "Логин должен содержать от 2 до 20 символов"  
    ]]

type Password = Password.T  

object Password  
    extends RefinedType[String, DescribedAs[  
      MinLength[8] & MaxLength[100],  
      "Пароль должен содержать от 8 до 100 символов"  
    ]]

Но в целом мы теперь рассказываем пользователю об ошибках и как их исправить.

Вместо заключения

Когда мы говорим о функциональном программировании, то пытаемся показать, что коду, написанному в ФП стиле, можно доверять. В том смысле, что он делает именно то, что написано и так, как написано.

Побочные эффекты — неотъемлемая часть промышленной разработки, но мы способны вынести их на верхний уровень так, чтобы поставить под свой контроль. HttpRoutes— это набор функции, а это значит, что мы можем легко покрыть код юнит-тестами, при этом не разворачивая для этого реальную инфраструктуру.

Мы используем уточняющие типы (UserName и Password), чтобы сделать невалидные состояния непредставимыми, чтобы пользователь даже не смог бы создать невалидную структуру данных. Это избавляет нас от колоссального набора ошибок, так как вся бизнес-логика будет реализована на уточняющих типах. Мы не перепутаем логин с паролем, не изменим их, не запихнем туда null.

Мы говорим абстракциями (UserService, UserRoutes), чтобы отделить поведение от реализации (Main). Это позволяет нам отдельно описывать то, что именно мы хотим сделать, и только потом переходить к тому, как это делать.

Использование уточняющих типов и накопительного декодирования позволяет сразу возвращать клиенту все ошибки валидации, а не первую попавшуюся. Это повышает удобство API и уменьшает количество повторных запросов

В конечном итоге всё это приводит к повышению доверия к написанному коду.

Ссылки: