Бывает полезно проводить валидацию данных из формы ввода и на фронте и на бэке, например чтобы не гонять лишний запрос с заведомо "плохими" данными. Отсюда появляется задача написания двух одинаковых валидаторов для фронта и бэка.
Если фронт и бэк написан на одном языке (привет js+node), то мы можем напрямую использовать один код валидатора и там, и там.
В остальных случаях (js+php, java, python, go, dotnet) есть проблема. Во-первых, придётся два раза писать примерно одно и то же на двух языках, во-вторых, нужно убедиться, что написанное работает одинаково. Особенно печальны случаи, когда фронт ошибочно зарезает данные, валидные с точки зрения бэка и логики приложения.
Проблему, конечно, можно решить аккуратной реализацией и вдумчивым покрытием валидаторов тестами, но я предлагаю опробовать немного другой подход.
Давайте будем использовать один ЯП для валидаторов даже если фронт и на бэк написаны на разных языках.
Создадим простое приложение из одной формы на PHP+Laravel, и добавим к нему немного фронта на JS. Пусть на фронте есть форма с полем "Имя". Корректное имя должно состоять из букв, первая буква должна быть заглавной, а остальные строчными.
Валидно: Иван, John
Не валидно: иван, ИВАН, ИваН, john jOHN JOHN
Реализуем валидацию на JS регулярными выражениями.
export function validateName(name) {
    if(!name.match(/^[A-ZА-ЯЁ][a-zа-яё]*$/u)) {
        return "Имя должно состоять из букв, "+
          "первая буква должна быть заглавной.";
    }
    return "";
}На фронте это работает.

Теперь прикрутим тот же валидатор к бэкэнду. Воспользуемся расширением FFI — создадим (конечно же берём готовый) интерпретатор JS в виде разделяемой библиотеки.
Мне показалось наиболее простым взять реализацию ECMAScript на чистом golang от Dmitry Panov.
Пропущу пару промежуточных шагов, там докерное шаманство лишь только, кому интересно — четыре этапа работы над кодом есть в репозитории.
Перехожу сразу к PHP. Итоговый валидатор Laravel выглядит так:
class ValidName implements ValidationRule
{
    protected RunJs $runjs;
    protected string $javascript;
    public function __construct(RunJs $runjs)
    {
        $this->runjs = $runjs;
        $javascript = file_get_contents(
          resource_path('js/validator.js')
        );
        $this->javascript = preg_replace(
          '#^export\sfunction#um', 
          'function',
          $javascript
        ) . ";";
    }
    public function validate(
      string $attribute, mixed $value, Closure $fail
    ): void
    {
        $code = $this->javascript . 
          'validateName("' . 
          addcslashes($value, '"') . 
          '");';
        $error = $this->runjs->RunJs($code);
        if (strlen($error > 0)) {
            $fail($error);
        }
    }
}Тут нюанс. Поскольку фронт у нас современный, то удобно использовать модули, но ECMAScript 5 на бэке вынуждает отказаться от синтаксиса export. Можно придумать и более красивое решение, но я решил просто вырезать слово export из исходника - в остальном js-код валидации на фронте и бэке идентичен.
Сервис запуска JS-кода выглядит так:
class RunJs
{
    protected FFI $ffi;
    protected FFI\CType $charPtr;
    
    public function __construct(string $libDir)
    {
        $this->ffi = FFI::cdef(
            file_get_contents($libDir . "/runjs_z.h"),
            $libDir . "/runjs.so",
        );
        $this->charPtr = $this->ffi->type('char *');
    }
    public function RunJs(string $code): string
    {
        $arg = $this->ffi->new("GoString");
        $arg->n = strlen($code);
        $str = $this->ffi->new(
          type: 'char[' . ($arg->n) . ']'
        );
        FFI::memcpy($str, $code, $arg->n);
        $arg->p = $this->ffi->cast($this->charPtr, $str);
        $res = $this->ffi->RunJs($arg);
        if ($res === null) {
            throw new Exception("JS Error");
        }
        $ret = FFI::string($res);
        FFI::free($res);
        return $ret;
    }
}Go-библиотека (runjs.go), которая вызывается по FFI, выглядит так:
package main
import "C"
import  "github.com/dop251/goja"
var vm *goja.Runtime = nil
func init() {
	vm = goja.New()
}
//export RunJs
func RunJs(script string) *C.char {
	val, err := vm.RunString(script)
	if err != nil {
		return nil
	}
	return C.CString(val.String())
}На бэке это тоже работает (фиолетовое сообщение было от фронта, а красное от бэка).

Успешная валидация:

Полный проект можно посмотреть здесь.
Выводы
Возможно использовать идею "единый код валидаторов для фронта и бэка", имея исходный код фронта на JS, а бэк не на node. Связка PHP + FFI + Go + JS — вполне рабочий вариант, хотя и не без недостатков.
О недостаках и проблемах
Первое. Код, написанный в этом эксперимента, не идеален, даже не сказать чтобы хорош. Брать из него куски и тащить в рабочий проект настоятельно не рекомендую. Очевидно присутствует проблема с производительностью и с безопасностью.
Второе. При первой попытке реализовать идею я использовал докер-контейнеры на базе alpine. Если кто-то пойдёт моим путём - остерегатесь этой проблемы: "runtime: c-shared builds fail with musllibc". Из-за неё сейчас (летом 2025) разделяемые библиотеки на golang не работают нормально в приложениях, слинкованных с mulibc.
Третье. К сожалению, поддержка юникодных регэкспов в пакте goja оказалась недостаточно глубока, а то можно было бы последовать совету из статьи "Хватит использовать [a-zа-яё]" и сделать совсем красиво.
export function validateName(name) {
    if(!name.match(/^\p{Lu}\p{Ll}*$/u)) {
        return "Имя должно состоять из букв, "+
          "первая буква должна быть заглавной.";
    }
    return "";
}Но увы-увы.
Спасибо за внимание, а теперь будут...
Ссылки
Репозиторий с кодом к этой статье
https://github.com/ein-gast/php-js-ffi
Вызываем функции Go из других языков
https://habr.com/ru/companies/vk/articles/324250/
Реализация ECMAScript на чистом golang
https://github.com/dop251/goja
Оптимизация размера Go-бинарника
https://habr.com/ru/companies/plesk/articles/532402/
Документация по модулю FFI в PHP
https://www.php.net/manual/ru/book.ffi.php
Туториал: использование Go из PHP через FFI
https://habr.com/ru/articles/902532/
Баг "runtime: c-shared builds fail with musllibc"
https://github.com/golang/go/issues/13492
Хватит использовать [a-zа-яё]: правильная работа с символами и категориями Unicode в регулярных выражениях
https://habr.com/ru/articles/713256/
Комментарии (23)
 - FanatPHP27.08.2025 06:01- Коллега, это гениально! Увидев только заголовок, мой не проснувшийся мозг построил другую траекторию - валидация на Go, в РНР через FFI, а в браузере - через WASM. Но так тоже хорошо! Вряд ли кто-то будет проделывать все эти телодвижения ради довольно простой и механически решаемой задачи (несколько строчек с правилами валидации) - особенно при наличии всех подводных камней, таких как устаревшая версия JS - но как идея это очень красиво! - Кстати, я сейчас подумал, что куда проще это всё решается двумя библиотеками с одинаковым синтаксисом валидации! Просто закинул с сервера массив - 'title' => 'required|string|max:255|unique:posts', 'body' => 'required|string', 'category_id' => 'required|integer|exists:categories,id', 'published_at' => 'nullable|date',- И JS по ним всё провалидировал. Конечно, всё равно будут проблемы с валидациями, требующими доступа к БД, но их можно игнорировать и сделать сервер-онли. 
 Впрочем, в каком-нибудь Livewire это наверняка уже реализовано? - GuestOne Автор27.08.2025 06:01- Согласен. Без вопросов. Есть и другие заходы на проблему, в том числе и два конструктора валидаторов из единого конфига. И использование третьего языа, который можно скомпилировать в язык и фронта и бэка. Можно посравнивать эти подходы на практике, но это уже будет другая статья. 
  - SbWereWolf27.08.2025 06:01- В 2020 была задача валидировать кучу произвольнах форм ввода, фронт-энд программист предложил на фронт кидать описание формы ввода, что бы создавать форму динамически в браузере из JS. Иначе ему приглось бы руками прописывать все базовые формы и потом тратииь время на разработку новых. Описание формы мы записывали как JSON. В описании формы ввода так же было описание правил валидации, в таком виде как я на бэке записывал их для Ларавель. - В итоге получилось делать формочку по спецификации из описания формы. Правила валидации были достаточно простыми: обязательность значения, тип, мин, макс, длина строки. Какие то специфичные проверялись только на бэке (например формат инн для юр лиц и для физ лиц) - При том что фронт получал описание формы с бэка у нас не было пролемы рассинхронизации проверок на сервере и в браузере. - Вариант с FFI идеальный, но требует индивидуального подхода. Для быстродействия с JS, получается надо выполнять сборку кода валиатора и написать динамическую подгрузку соотвествующих бинарников. - Своё решение с компиляцией на go если доведёте до ума, то думаю кто то решиться,попробовать применить. - Успехов ! - PS - https://bun.sh/docs/bundler/executables - Инструкция как из JS кода сделать исполняемый файл под любую операционку. 
  - dab181827.08.2025 06:01- или xsd. как язык описания проверок. - на сервере много лет работает вариант на php с простой трансляцией form-url-encoded в xml и дальнейшей проверке по схемам. - а для клиента был вариант с генерацией валидаторов на js из тех же схем... - а еще раньше это можно было декларативно прям в xforms описывать - и биндинги элементов к данным и т.д. 
 
 - savostin27.08.2025 06:01- Даже не знаю, не разделяю восхищение. Чтобы разработчику меньше набирать и не ошибиться, вы заставляете в рантайме гонять JavaScript на каждый запрос клиента в считай виртуальной машине. И правда кодогенераторы выглядят более разумным решением. Кстати, есть Zod для Go, правда как и положено в Go, со своим синтаксисом, ибо не дай бог разработчику надо набрать слово public, пусть не напрягается и просто жмет Shift - public функции с заглавной :-/  - GuestOne Автор27.08.2025 06:01- Спасибо за ссылочку. Для того и общаемся, чтобы в комментариях насыпали того, на что сам не сразу наскочишь. 
  - grinsv27.08.2025 06:01- Согласен. Кодогенерация на основе спецификации OpenAPI точно есть в Go и в Laravel. Для фронта наверняка тоже есть решения. - Пишешь yaml, на его основе генерируешь код с валидацией. 
 
 - abyrvalg27.08.2025 06:01- Забавный эксперимент, но что делать, если участников больше двух? Например, добавляется мобилка на флаттере. - Мне больше нравится вариант с использованием общего формата описания правил валидации. Тем более, что самому с нуля формат описания правил можно и не придумывать, а взять готовый https://json-schema.org Стандартная схема позволяет описывать достаточно сложные случаи. Не хватает - всегда можно расширить.  - GuestOne Автор27.08.2025 06:01- А вот это хороший вопрос. Не думал, что делать для 3+ участников. Подумаю. 
 Что касается схемы, это понятно, что в 95% случаев использование библиотек с единой конфигурацией наиболее разумно. Я исследую вопрос с другого угла, хочу чтобы валидатор был не декларативными правилами, а исполнимым кодом. Так гибкость выше чем у любого конструктора правил.
 
 - izibrizi227.08.2025 06:01- А как насчет валидации, например, емейл уже такой зареган, ну и другие случаи, когда нужно сходить в базу. Или одно поле зависит от другого: если выбрали галочку "юр лицо" то появляются доп поля, где нужно валидировать инн.  - GuestOne Автор27.08.2025 06:01- Если нужно сходить в БД — это в любом случае не фронтовый валидатор, "не наш случай". "Одно поле зависит от другого" — это задача валидации, "появляются доп поля" — задача уже вне валидации. Я начал вторую часть статьи, реализация того же, но на wasm, попробую проанализировать в процессе случай с зависимыми полями, как он соотносится с идеей "один код валидации на фронте и бэке".  - izibrizi227.08.2025 06:01- Что значит не наш случай, если это основная головная боль? :) Когда у вас появдяется зависимость одного поля от другого, это поле тоже надо валидировать. Да даже взять валидацию, когда поле становится required в зависимости от другого поля. Это 99% кода, а вы так аккуратно избегаете этого аспекта. Просто так проверить формат - ну комон, этого добра воз и маленькая тележка. Проще уж тогда валидировать на сервере и присылать ошиьэбки, чем дублировать валидирование  - GuestOne Автор27.08.2025 06:01- Не уверен что следует вам ответить. Пример зависимых валидаторов сделаю во второй статье. Про валидацию на бэке — первый абзац этой статьи. Что выбранный мной подход не прост, не очевиден и не является "первым выбором" — да, именно по этому я его взял и исследую. 
 
 
 
 - juDge27.08.2025 06:01- Почему не использовать например функционал json schema? Я например на стороне сервера уже лет 10 использую для проекта на php. Немного правда пришлось добавить функционала кастомных, чтобы при нескольких проверках выдавало ошибку именно по этой проверке а не просто общую ошибку. - Сейчас новый проект но уже на сервере на rust писан. Так же json схемы входящие данные проверяют  - GuestOne Автор27.08.2025 06:01- Можно и разумно использовать json schema или решения с аналогичным подходм, согласен. Я, просто, решил подойти к вопросу чуть шире и с чуть менее серьёзным лицом. 
 
 
           
 

karrakoliko
Любопытный трюк!
Наверное, более правильным решением будем иметь единый список правил валидации в json, одинаково интерпретируемый и бэком и фронтом
FanatPHP
Бинго, мне такая же идея пришла в голову!
GuestOne Автор
Да, безусловно. Приминимость разных подходв зависит от размера проекта и сложности валидаторов. Мой эксперимент, как правилнее сказать, - "на максималочках" - на JS-то точно любой валидатор можно написать. Ну и так веселее :)