Skip to main content

NGINX

NGINX JavaScript (njs): Add Scripting to NGINX

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

NGINX JavaScript, commonly known as njs, transforms NGINX from a static configuration-based server into a programmable platform. With njs, system administrators can write custom request handlers, implement complex access control logic, aggregate data from multiple sources, and create powerful API gateways—all using familiar JavaScript syntax.

This comprehensive guide covers everything you need to know about the NGINX JavaScript module, from installation to advanced scripting patterns with fully tested, production-ready examples.

Why Use NGINX JavaScript?

Traditional NGINX configuration is powerful but limited. When you need to:

  • Implement custom authentication logic
  • Transform request or response data
  • Aggregate data from multiple backend services
  • Create dynamic routing based on complex conditions
  • Build lightweight API endpoints without a backend

You traditionally had two choices: write a complex Lua module or route requests to an application server. For maximum performance, you could also embed native code using the NGINX Link Function module for C/C++ extensions. NGINX JavaScript offers a more accessible option: write the logic directly in JavaScript, a language most developers already know.

For real-time features like WebSockets, pub/sub messaging, and long-polling, see NGINX Nchan which provides built-in support for these patterns without custom code.

Key Advantages of njs

Familiar Syntax: If you know JavaScript, you can extend NGINX immediately. No need to learn Lua or C.

High Performance: njs runs inside the NGINX worker process with minimal overhead. Scripts are precompiled at configuration load time.

Rich Feature Set: Access request/response data, make subrequests, interact with shared memory, schedule periodic tasks, and even fetch external URLs.

Security: njs runs in a sandbox with no access to the filesystem or network beyond explicitly configured connections.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the module from the GetPageSpeed repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-njs

Enable the module in your NGINX configuration by adding the following to /etc/nginx/nginx.conf:

load_module modules/ngx_http_js_module.so;

If you also need stream (TCP/UDP) support:

load_module modules/ngx_stream_js_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-njs

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

Verify Installation

Check that the module is loaded:

nginx -V 2>&1 | grep njs

You can also use the standalone njs command-line tool for testing scripts:

njs -v
# Output: 0.9.5

njs -e "console.log('Hello from njs!')"

QuickJS Engine – Full ES2023 Support

The GetPageSpeed njs package (only RPM at present) includes QuickJS-NG engine support, giving you access to the complete ES2023 JavaScript specification. This is a major advantage over the default njs engine which only supports ES5 with select ES6+ extensions.

Enabling QuickJS Engine

Add the js_engine directive to your http block:

load_module modules/ngx_http_js_module.so;

http {
    js_engine qjs;  # Enable QuickJS for ES2023 support

    js_import main from /etc/nginx/njs/main.js;

    server {
        listen 80;
        # ...
    }
}

ES2023 Features Available with QuickJS

With js_engine qjs; enabled, you can use modern JavaScript features:

BigInt for Large Numbers:

function handleBigNumbers(r) {
    const big = BigInt(9007199254740993);
    const result = big * 2n;
    r.return(200, `Result: ${result.toString()}\n`);
}

Arrow Functions and const/let:

function processData(r) {
    const numbers = [1, 2, 3, 4, 5];
    const doubled = numbers.map(x => x * 2);
    const sum = doubled.reduce((a, b) => a + b, 0);
    r.return(200, `Sum: ${sum}\n`);
}

Destructuring:

function parseRequest(r) {
    const { method, uri, args } = r;
    const [first, ...rest] = Object.keys(args);
    r.return(200, JSON.stringify({ method, uri, first, rest }));
}

Template Literals:

function formatResponse(r) {
    const name = "NGINX";
    const version = "1.28.2";
    r.return(200, `Running ${name} v${version}
Multi-line strings work too!
Math: 2 + 2 = ${2 + 2}\n`);
}

Spread Operator:

function mergeData(r) {
    const defaults = { timeout: 30, retries: 3 };
    const custom = { timeout: 60 };
    const config = { ...defaults, ...custom };
    r.return(200, JSON.stringify(config));
}

Optional Chaining and Nullish Coalescing:

function safeAccess(r) {
    const data = JSON.parse(r.requestText);
    const name = data?.user?.profile?.name ?? "Anonymous";
    r.return(200, `Hello, ${name}!\n`);
}

Async/Await with Promises:

async function asyncHandler(r) {
    const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
    await delay(10);
    r.return(200, "Async operation complete!\n");
}

ES6 Classes:

class RequestHandler {
    constructor(request) {
        this.r = request;
    }

    respond(status, body) {
        this.r.return(status, body);
    }
}

function handleWithClass(r) {
    const handler = new RequestHandler(r);
    handler.respond(200, "Response from class method!\n");
}

Standalone QuickJS CLI

GetPageSpeed also provides the standalone quickjs package for developing and testing scripts:

sudo dnf install quickjs

Test your scripts before deploying to NGINX:

# Test ES2023 features
qjs -e "const arr = [1,2,3]; console.log(arr.map(x => x*2))"
# Output: 2,4,6

# Test BigInt
qjs -e "console.log(BigInt(9007199254740993))"
# Output: 9007199254740993n

# Run script files
qjs myscript.js

Getting Started: Your First njs Handler

Create a JavaScript file at /etc/nginx/njs/main.js:

function hello(r) {
    r.return(200, "Hello from NGINX JavaScript!\n");
}

export default { hello };

Configure NGINX to use this handler:

load_module modules/ngx_http_js_module.so;

http {
    js_engine qjs;  # Recommended: Enable QuickJS for modern JavaScript
    js_import main from /etc/nginx/njs/main.js;

    server {
        listen 80;

        location / {
            js_content main.hello;
        }
    }
}

Test the configuration and reload:

nginx -t && systemctl reload nginx
curl http://localhost/
# Output: Hello from NGINX JavaScript!

Core Directives Reference

js_engine

Selects the JavaScript engine. The GetPageSpeed package supports both engines:

  • njs (default): Built-in engine, ES5 with select ES6+ extensions
  • qjs: QuickJS-NG engine with full ES2023 support
js_engine qjs;  # Recommended for modern JavaScript

Context: http, server, location

js_import

Imports a JavaScript module for use in the configuration.

js_import main from /etc/nginx/njs/main.js;
js_import utils from /etc/nginx/njs/utils.js;

Context: http, server, location

js_content

Specifies a JavaScript function to handle the request and generate the response.

location /api {
    js_content main.apiHandler;
}

Context: location

js_set

Creates an NGINX variable whose value is computed by a JavaScript function.

js_set $client_type main.getClientType;

location / {
    add_header X-Client-Type $client_type;
}

Context: http, server, location

js_var

Declares a variable accessible from JavaScript code.

js_var $custom_var "default_value";

Context: http, server, location

js_header_filter

Specifies a JavaScript function to modify response headers before they are sent. For adding security headers to NGINX, you can use either njs or the dedicated headers-more module.

location / {
    js_header_filter main.addSecurityHeaders;
    proxy_pass http://backend;
}

Context: location

js_body_filter

Specifies a JavaScript function to transform the response body.

location / {
    js_body_filter main.transformBody;
    proxy_pass http://backend;
}

Context: location

js_shared_dict_zone

Defines a shared memory zone accessible from JavaScript for storing key-value data.

js_shared_dict_zone zone=cache:10M;
js_shared_dict_zone zone=counters:1M timeout=60s;

Context: http

js_periodic

Schedules a JavaScript function to run at regular intervals.

location @cleanup {
    js_periodic main.cleanup interval=60s;
}

Context: location

js_path

Specifies additional paths for module resolution.

js_path /etc/nginx/njs/lib;

Context: http, server, location

Practical Examples

Example 1: JSON API Endpoint

Create a complete JSON API endpoint that returns request information:

// /etc/nginx/njs/api.js
function jsonApi(r) {
    const response = {
        method: r.method,
        uri: r.uri,
        args: r.args,
        remoteAddress: r.remoteAddress,
        httpVersion: r.httpVersion,
        timestamp: new Date().toISOString()
    };

    r.headersOut["Content-Type"] = "application/json";
    r.return(200, JSON.stringify(response, null, 2) + "\n");
}

export default { jsonApi };
js_import api from /etc/nginx/njs/api.js;

location /api/info {
    js_content api.jsonApi;
}

Test output:

{
  "method": "GET",
  "uri": "/api/info",
  "args": {
    "name": "test"
  },
  "remoteAddress": "127.0.0.1",
  "httpVersion": "1.1",
  "timestamp": "2026-02-11T08:53:28.603Z"
}

Example 2: Token-Based Authorization

Implement a simple bearer token authentication handler. For production environments requiring robust authentication, consider using the NGINX JWT authentication module for standards-compliant token validation.

// /etc/nginx/njs/auth.js
function authorize(r) {
    const token = r.headersIn["Authorization"];

    if (!token) {
        r.return(401, "Authorization header required\n");
        return;
    }

    if (token === "Bearer secret-token-123") {
        r.return(200, "Access granted!\n");
    } else {
        r.return(403, "Invalid token\n");
    }
}

export default { authorize };
js_import auth from /etc/nginx/njs/auth.js;

location /protected {
    js_content auth.authorize;
}

Example 3: Request Body Processing

Process JSON POST requests and transform the data:

// /etc/nginx/njs/process.js
function processJson(r) {
    r.headersOut["Content-Type"] = "application/json";

    try {
        const body = JSON.parse(r.requestText);
        const response = {
            received: body,
            processed: true,
            timestamp: new Date().toISOString(),
            keys: Object.keys(body).length
        };
        r.return(200, JSON.stringify(response, null, 2) + "\n");
    } catch (e) {
        r.return(400, JSON.stringify({
            error: "Invalid JSON",
            message: e.message
        }) + "\n");
    }
}

export default { processJson };

Test:

curl -X POST -H "Content-Type: application/json" \
  -d '{"name":"John","age":30}' \
  http://localhost/process

Example 4: Subrequest Aggregation

Aggregate data from multiple internal services into a single response:

// /etc/nginx/njs/aggregate.js
async function aggregateData(r) {
    r.headersOut["Content-Type"] = "application/json";

    try {
        const [resp1, resp2] = await Promise.all([
            r.subrequest("/internal/service1"),
            r.subrequest("/internal/service2")
        ]);

        const combined = {
            service1: JSON.parse(resp1.responseText),
            service2: JSON.parse(resp2.responseText),
            aggregated_at: new Date().toISOString()
        };

        r.return(200, JSON.stringify(combined, null, 2) + "\n");
    } catch (e) {
        r.return(500, JSON.stringify({
            error: "Aggregation failed",
            message: e.message
        }) + "\n");
    }
}

export default { aggregateData };
js_import agg from /etc/nginx/njs/aggregate.js;

location /aggregate {
    js_content agg.aggregateData;
}

location /internal/service1 {
    internal;
    return 200 '{"name": "Service 1", "status": "healthy"}';
}

location /internal/service2 {
    internal;
    return 200 '{"name": "Service 2", "connections": 42}';
}

Example 5: Shared Dictionary for Counters

Use shared memory to implement a request counter across workers:

// /etc/nginx/njs/counter.js
function incrementCounter(r) {
    r.headersOut["Content-Type"] = "application/json";

    const counter = ngx.shared.stats;
    const currentCountStr = counter.get("request_count");
    const currentCount = currentCountStr ? parseInt(currentCountStr) : 0;
    const newCount = currentCount + 1;

    counter.set("request_count", String(newCount));

    r.return(200, JSON.stringify({
        request_number: newCount,
        timestamp: new Date().toISOString()
    }, null, 2) + "\n");
}

export default { incrementCounter };
js_shared_dict_zone zone=stats:1M;
js_import counter from /etc/nginx/njs/counter.js;

location /counter {
    js_content counter.incrementCounter;
}

Example 6: External HTTP Requests with Fetch API

Make HTTP requests to external services using the Fetch API:

// /etc/nginx/njs/fetch.js
async function fetchExternal(r) {
    r.headersOut["Content-Type"] = "application/json";

    try {
        const response = await ngx.fetch("http://httpbin.org/get", {
            method: "GET",
            headers: { "User-Agent": "NGINX-njs/0.9" }
        });

        if (response.ok) {
            const body = await response.text();
            r.return(200, JSON.stringify({
                status: response.status,
                external_response: JSON.parse(body)
            }, null, 2) + "\n");
        } else {
            r.return(response.status, JSON.stringify({
                error: "External request failed"
            }) + "\n");
        }
    } catch (e) {
        r.return(500, JSON.stringify({
            error: "Fetch failed",
            message: e.message
        }) + "\n");
    }
}

export default { fetchExternal };
js_import fetch from /etc/nginx/njs/fetch.js;

server {
    resolver 8.8.8.8;

    location /fetch {
        js_content fetch.fetchExternal;
    }
}

Important: External fetch requires:
1. A resolver directive for DNS resolution
2. SELinux boolean httpd_can_network_connect enabled (on RHEL-based systems):

setsebool -P httpd_can_network_connect 1

Performance Considerations

Context Reuse

Enable context reuse to avoid recompiling JavaScript on every request:

js_context_reuse 128;
js_context_reuse_max_size 16m;

This caches up to 128 JavaScript contexts per worker, significantly improving performance for high-traffic locations.

QuickJS Performance

With context reuse enabled, QuickJS performs within 1% of the njs engine while providing full ES2023 support. The slight overhead is negligible for most use cases.

Shared Dictionary Sizing

Size your shared dictionaries appropriately:

# Small cache for counters
js_shared_dict_zone zone=counters:1M;

# Larger cache for session data
js_shared_dict_zone zone=sessions:32M timeout=3600s evict;

The timeout parameter enables automatic expiration, and evict allows removal of oldest entries when the zone is full.

Security Best Practices

Input Validation

Always validate user input in your JavaScript handlers:

function processInput(r) {
    const input = r.args.data;

    if (!input || input.length > 1000) {
        r.return(400, "Invalid input\n");
        return;
    }

    // Process validated input
}

Error Handling

Wrap operations in try-catch blocks to prevent crashes:

function safeHandler(r) {
    try {
        const data = JSON.parse(r.requestText);
        r.return(200, "OK");
    } catch (e) {
        r.return(400, `Error: ${e.message}`);
    }
}

Limit External Connections

Configure timeouts and limits for fetch requests:

js_fetch_timeout 10s;
js_fetch_max_response_buffer_size 1m;
js_fetch_buffer_size 16k;

Troubleshooting

Common Errors

“SyntaxError: Unexpected token”: Check your JavaScript syntax. Use the njs or qjs CLI to validate:

njs -c "your code here"
qjs myscript.js

“no resolver defined”: Add a resolver directive when using ngx.fetch():

server {
    resolver 8.8.8.8;
}

“Permission denied” on fetch: Enable SELinux network connections:

setsebool -P httpd_can_network_connect 1

“string value is expected”: Shared dictionaries only store strings. Convert numbers:

counter.set("count", String(42));

Debugging

Enable debug logging and check the error log:

error_log /var/log/nginx/error.log debug;

Log messages from JavaScript:

ngx.log(ngx.INFO, `Debug message: ${JSON.stringify(data)}`);
ngx.log(ngx.ERR, `Error occurred: ${e.message}`);

Conclusion

NGINX JavaScript provides powerful scripting capabilities that bridge the gap between simple configuration and full application development. With GetPageSpeed’s njs package including QuickJS-NG engine support, you get the best of both worlds: the performance of NGINX with modern ES2023 JavaScript features.

Whether you need custom authentication, response transformation, or complex routing logic, njs delivers the flexibility of JavaScript with the performance of NGINX.

For the complete module package information, visit:
RPM packages: nginx-extras.getpagespeed.com/modules/njs/
APT packages: apt-nginx-extras.getpagespeed.com/modules/njs/
Source code: github.com/nginx/njs

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.