Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

njs для Nginx: небольшой JavaScript для хитрой логики без бэкенда

njs — это встраиваемый JavaScript‑движок для Nginx, позволяющий добавить немного логики на уровне веб‑сервера. В статье показано, как включить модуль, разобрать объект r, применить js_set/js_content/js_header_filter/js_access и на примерах реализовать маршрутизацию, заголовки и простую авторизацию.
njs для Nginx: небольшой JavaScript для хитрой логики без бэкенда

njs в Nginx: JavaScript-функции внутри конфигурации веб-сервера

Зачем njs в 2025 году и что это такое

njs — это лёгкий JavaScript‑движок, встроенный в Nginx через динамические модули ngx_http_js_module и ngx_stream_js_module. Он позволяет внедрять в конфигурацию немного прикладной логики: вычислять переменные на лету, условно проксировать запросы в разные backend‑ы, править заголовки и даже формировать ответ без участия внешнего приложения. Это не замена полноценному бэкенду, а инструмент «на краю», там, где требуется компактное и быстрое решение.

Когда njs уместен:

  • Микро‑обработчики на периметре: A/B‑маршрутизация, canary‑трафик, проброс специальных заголовков.
  • Условные редиректы и каноникализация URL/хостов с учётом множества факторов.
  • Быстрые ответы для health‑check, ping, метаданных, JSON‑заглушек.
  • Тонкая настройка headers на ответ и запрос, где стандартных директив не хватает.
  • Предобработка для API: простая авторизация по ключу, верификация формата аргументов.

Главная идея njs — не заменить приложение, а разгрузить его от рутинной логики, которую дешевле и быстрее выполнить в самом Nginx.

Установка и включение модулей

В большинстве дистрибутивов njs доступен как динамический модуль. На практике понадобятся два шага: установить пакет и явно подгрузить модуль в nginx.conf. На типовом проде удобнее управлять этим на VDS, где есть root и контроль над версиями Nginx. На классическом shared‑хостинге модуль может быть недоступен.

Примеры установки по семействам ОС

Debian/Ubuntu:

apt update
apt install libnginx-mod-http-js libnginx-mod-stream-js

Альтернативные сборки Nginx (mainline) могут называть пакет иначе, например nginx-module-njs. Суть та же: модуль для HTTP и, при необходимости, для Stream.

RHEL/AlmaLinux/Rocky:

dnf install nginx-module-njs

Подключение динамических модулей

Проверьте путь к модулям в вашей системе. Часто это /usr/lib/nginx/modules. В начале nginx.conf пропишите:

load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;

Если нужен только HTTP, вторую строку можно опустить.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Быстрый тур по примитивам njs

Ключевые директивы:

  • js_import — подключает JS‑модуль (файл) и даёт ему псевдоним.
  • js_set — вызывает JS‑функцию для вычисления значения nginx‑переменной.
  • js_content — отдаёт контент из JS‑функции (контент‑хендлер).
  • js_header_filter — фильтр ответных заголовков.
  • js_access — проверка доступа в фазе access (например, авторизация по ключу).

Объект запроса r внутри функций предоставляет:

  • r.method, r.uri, r.args, r.variables (Nginx‑переменные по имени),
  • r.headersIn и r.headersOut (входящие/исходящие заголовки),
  • r.return(status, body) для ответа, r.log(msg) и r.warn(msg) для логов,
  • полезны также r.internalRedirect, r.subrequest() в более сложных схемах.

Редактирование конфигурации Nginx и njs-модуля на сервере

Структура проекта и подключение файла с кодом

Рекомендуемая структура:

  • Хранить njs‑файлы отдельно, например /etc/nginx/njs/http.js.
  • Импортировать модуль на уровне http { ... } один раз.
  • Функции экспортировать именованно или через export default.
# /etc/nginx/nginx.conf (фрагмент)
load_module modules/ngx_http_js_module.so;

http {
    js_import http from /etc/nginx/njs/http.js;

    server {
        listen 80;
        server_name example.local;

        location = /healthz {
            js_content http.hello;
        }
    }
}
// /etc/nginx/njs/http.js
function hello(r) {
    r.headersOut['Content-Type'] = 'application/json; charset=utf-8';
    r.return(200, JSON.stringify({ status: 'ok', ts: Date.now() }));
}

export default { hello };

После правок JS‑файла делайте перезагрузку конфигурации:

nginx -t
nginx -s reload

Пример 1. Динамический выбор апстрима по cookie/заголовку

Задача: размазывать трафик между двумя версиями приложения на основании cookie exp или заголовка. В njs вычислим переменную $backend, а proxy_pass подставит её в runtime.

# /etc/nginx/nginx.conf (фрагмент)
http {
    js_import http from /etc/nginx/njs/http.js;

    upstream app_v1 { server 127.0.0.1:8081; }
    upstream app_v2 { server 127.0.0.1:8082; }

    server {
        listen 80;
        server_name example.local;

        js_set $backend http.choose_upstream;

        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://$backend;
        }
    }
}
// /etc/nginx/njs/http.js
function getCookie(r, name) {
    const c = r.headersIn['Cookie'];
    if (!c) return null;
    const m = c.match(new RegExp('(?:^|; )' + name + '=([^;]+)'));
    return m ? decodeURIComponent(m[1]) : null;
}

function choose_upstream(r) {
    const header = r.headersIn['X-Exp'] || '';
    const cookie = getCookie(r, 'exp') || '';

    // приоритет заголовка, потом cookie
    const mark = (header || cookie).toLowerCase();
    if (mark === 'v2' || mark === 'beta') return 'app_v2';

    // простая псевдослучайная раскатка 10% на v2
    const h = (r.variables.request_id || r.remoteAddress || Date.now()).toString();
    const bucket = Math.abs(hashCode(h)) % 10;
    return bucket === 0 ? 'app_v2' : 'app_v1';
}

function hashCode(s) {
    let h = 0;
    for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
    return h;
}

export default { choose_upstream };

Такой подход позволяет точно контролировать трафик без внешней зависимости. При этом всё прозрачно в конфигурации и логах Nginx.

Пример 2. Каноникализация хоста и схемы одной функцией

Стандартные return/rewrite покрывают 90% кейсов, но как только правил несколько и они зависят друг от друга, удобнее собрать их в одну JS‑функцию, чтобы не городить каскад директив.

# nginx.conf (фрагмент)
server {
    listen 80;
    listen 443 ssl;

    # ssl_* директивы опущены ради краткости

    location / {
        js_content http.canonical_redirect;
    }
}
// http.js (фрагмент)
function canonical_redirect(r) {
    const host = (r.headersIn['Host'] || '').toLowerCase();
    const scheme = r.variables.scheme;
    let targetHost = host.replace(/^www\./, '');

    // Принудительно HTTPS и без www
    const needHttps = scheme !== 'https';
    const needHost = host !== targetHost;

    if (needHttps || needHost) {
        const url = 'https://' + targetHost + r.uri + (r.args ? '?' + r.args : '');
        r.return(301, url);
        return;
    }

    // Если всё уже канонично, отдаём контент бэкенда
    r.internalRedirect('@upstream');
}

export default { canonical_redirect };
# в server {} добавьте внутренний location для проксирования
location @upstream {
    proxy_set_header Host $host;
    proxy_pass http://app_v1;
}

Если вы как раз готовите принудительный HTTPS и HSTS, посмотрите материал «миграция на HTTPS и HSTS» — это поможет не допустить лишних редиректов и кэш‑проблем, а также подобрать правильные сроки политики HSTS: миграция на HTTPS и HSTS. Для продакшен‑домена заранее оформите актуальные SSL-сертификаты.

Пример 3. Единый фильтр ответных заголовков

Когда нужно гибко добавлять/менять заголовки ответа в зависимости от пути, метода или исхода обработки, удобен js_header_filter. Например, добавим ряд безопасных заголовков только для HTML‑страниц.

# nginx.conf (фрагмент)
http {
    js_import http from /etc/nginx/njs/http.js;
    js_header_filter http.add_security_headers;

    server { ... }
}
// http.js (фрагмент)
function isHtml(r) {
    const ct = (r.headersOut['Content-Type'] || '').toLowerCase();
    return ct.includes('text/html');
}

function add_security_headers(r) {
    if (!isHtml(r)) return;
    if (!r.headersOut['X-Frame-Options']) r.headersOut['X-Frame-Options'] = 'SAMEORIGIN';
    if (!r.headersOut['X-Content-Type-Options']) r.headersOut['X-Content-Type-Options'] = 'nosniff';
    if (!r.headersOut['Referrer-Policy']) r.headersOut['Referrer-Policy'] = 'strict-origin-when-cross-origin';
}

export default { add_security_headers };

Про CORS заголовки и кросс‑доменные шрифты/скрипты — отдельный разбор: настройка CORS в Nginx и Apache.

Поток запроса через Nginx с njs-фильтрами заголовков и тела

Пример 4. Простая авторизация API‑ключом на уровне access

Для внутренних API иногда достаточно валидировать заголовок X-API-Key. Сделаем это в фазе access и пустим запрос дальше только при успехе.

# nginx.conf (фрагмент)
location /api/ {
    js_access http.check_api_key;
    proxy_pass http://api_backend;
}
// http.js (фрагмент)
const ALLOWED_KEYS = new Set(['key123', 'key456']);

function check_api_key(r) {
    const key = r.headersIn['X-API-Key'];
    if (!key || !ALLOWED_KEYS.has(key)) {
        r.return(403, 'Forbidden');
        return;
    }
    // Ничего не возвращаем — запрос продолжит обработку
}

export default { check_api_key };

Важно: в реальных сценариях ключи храните вне репозитория конфигурации, используйте переменные окружения или внешние секреты, а также аудит попыток.

Пример 5. Мини‑фильтр тела ответа

njs умеет фильтровать тело ответа. Осторожный пример: маскировка электронных адресов в JSON/HTML. Производительность здесь критична, используйте на малых объёмах и точечно.

# nginx.conf (фрагмент)
http {
    js_import http from /etc/nginx/njs/http.js;

    server {
        listen 80;
        js_header_filter http.add_security_headers;
        js_body_filter http.mask_emails;
        location / { proxy_pass http://app_v1; }
    }
}
// http.js (фрагмент)
function mask_emails(r, data, flags) {
    // Фильтр обязан передать буфер дальше: либо модифицированный, либо как есть
    try {
        const replaced = data.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+/g, '***@***');
        r.sendBuffer(replaced, flags);
    } catch (e) {
        r.warn('mask_emails error: ' + e);
        r.sendBuffer(data, flags);
    }
}

export default { mask_emails };

Для тяжёлых трансформаций лучше перенести логику в бэкенд или специализированный фильтр.

Отладка и тестирование njs‑кода

Минимальный цикл отладки:

  • Понизьте уровень шума в error_log до notice/info и используйте r.log()/r.warn() внутри функций.
  • Проверяйте синтаксис: nginx -t.
  • Перезагружайте конфигурацию: nginx -s reload.
  • Локально воспроизводите входные данные: заголовки, cookie, URI — это ускоряет поиск ошибок.

Отдельная утилита интерпретатора njs (если установлена в системе) позволяет быстро проверить чистую JS‑логику и регулярные выражения, но доступ к объекту r вне Nginx недоступен.

Производительность и ограничения

Несколько практических правил:

  • njs‑функция вызывается на каждый запрос/ответ в соответствующей фазе. Держите код небольшим и детерминированным.
  • Избегайте тяжёлых вычислений и работы с крупными буферами в js_body_filter — это упирается в CPU и латентность.
  • Храните таблицы и константы в модуле, а не генерируйте их заново на каждый вызов.
  • Старайтесь работать строками и простыми структурами. Сложный парсинг JSON возможен, но контролируйте размеры.
  • Это не Node.js: нет ввода‑вывода, файлов и сокетов из JS. Всё взаимодействие — через API Nginx.

По сравнению с альтернативами:

  • OpenResty (lua) — богаче экосистема и возможностей, но сложнее. njs — проще для «точечных» задач.
  • Nginx Unit — отдельный рантайм приложений. njs быстрее стартует и ближе к конфигурации Nginx.
  • map/if/try_files — сначала попытайтесь решить задачу штатными директивами. njs — когда этих средств уже недостаточно.

Безопасность

Несмотря на компактность, njs влияет на критический путь обработки запроса:

  • Валидация входных данных обязательна: не доверяйте заголовкам и параметрам, ограничивайте длину и формат.
  • Ошибки в коде не должны «ронять» пайплайн: окружайте уязвимые участки try/catch и логируйте.
  • Не внедряйте чувствительную бизнес‑логику, если она требует секрета на стороне сервера — храните секреты вне конфигурации и применяйте контроль доступа.
  • Разделяйте зоны ответственности: фильтры тела — только там, где без них никак.

Типичные ошибки и как их избежать

  • Модуль не подгружается: проверьте путь load_module и наличие пакета. Сообщения ищите в error_log при старте.
  • Функция не видна: совпадает ли имя в js_import и экспорт в файле (export default { fn } или явный экспорт)?
  • Переменная пустая: для js_set возвращайте строку из функции. Не вызывайте r.return() внутри вычислителя переменной.
  • Циклы редиректов: следите, чтобы canonical_redirect прекращал редирект, когда всё уже канонично.
  • Регулярки «задушили» CPU: кешируйте шаблоны, упрощайте выражения, ограничивайте область применения фильтра.

Паттерны, которые часто «выстреливают»

  • Маршрутизация по версии клиента: cookie/заголовки — в js_set переменную, затем proxy_pass по имени апстрима.
  • Единый ответ для ping/health: js_content с JSON и минимальными полями для мониторинга.
  • Условные security headers: общий js_header_filter с проверкой типа контента и путей.
  • Простая авторизация: js_access для ключей/токенов с чётким журналированием.
  • Нормализация URL: функция, аккуратно объединяющая правила, чтобы не плодить if и rewrite.

Итоги

njs уверенно закрывает слой «умной конфигурации» Nginx: быстрая маршрутизация, тонкие правила для заголовков, компактные ответы и простые проверки доступа. Это инструмент для админов и девопсов, которым важно реагировать быстро и не тащить в бой лишние компоненты. Прежде чем тянуть бэкенд ради мелочи, попробуйте njs — часто это тот самый «небольшой JavaScript» на периметре, которого достаточно.

Поделиться статьей

Вам будет интересно

s5cmd vs AWS CLI: скорость работы с S3 на VDS и удобство команд OpenAI Статья написана AI Fastfox

s5cmd vs AWS CLI: скорость работы с S3 на VDS и удобство команд

Что выбрать для массовых операций с S3 на VDS: s5cmd или AWS CLI? Разбираем производительность, параллелизм и удобство, приводим м ...
Traefik vs Nginx vs Caddy: маршрутизация и TLS в контейнерных стэках OpenAI Статья написана AI Fastfox

Traefik vs Nginx vs Caddy: маршрутизация и TLS в контейнерных стэках

Практичный разбор reverse proxy для Docker: где уместны Traefik, Nginx и Caddy, как они решают маршрутизацию, авто‑TLS, HTTP/3, gR ...
NFS vs SSHFS для общего хранилища медиа: что выбрать и почему OpenAI Статья написана AI Fastfox

NFS vs SSHFS для общего хранилища медиа: что выбрать и почему

Выбираете общий storage для медиа между несколькими серверами и колеблетесь между NFS и SSHFS? Разбираем архитектуру, производител ...