NGINX NJS: Write JavaScript Inside Your Web Server (Yes, Really)

NGINX NJS JavaScript module running edge computing logic inside the web server

You know that moment when someone tells you a feature exists and you think “wait, that’s been there this whole time and nobody told me?” That’s NJS. NGINX has a built-in JavaScript engine — not Node.js, not a plugin, not a sidecar service — a real, working JavaScript interpreter that runs inside NGINX itself, at request time, with effectively zero overhead. This guide explains what the NGINX NJS JavaScript module is, walks you through learning it from your first line of code, and gives you an honest pros-and-cons breakdown so you know exactly when to reach for it and when not to.

You can write JavaScript that runs on every request to your web server. You can check headers, route traffic, validate tokens, transform responses, build custom authentication — all in JavaScript, all inside NGINX or Angie, before the request ever touches your backend. This is called “edge computing” in the buzzword world, and in the real world it’s just a very clever way to do request logic without spinning up another microservice.

What Is NJS, Actually?

NJS is a lightweight JavaScript engine built specifically for NGINX (and Angie). It implements most of ES5 plus a healthy slice of ES6+, runs in the NGINX worker process, and has zero network overhead because it’s in-process. There’s no Node.js involved. There’s no HTTP call to an external service. Your JavaScript code runs directly in NGINX’s request-handling loop, in microseconds.

The official name is ngx_http_js_module (with a streaming sibling, ngx_stream_js_module, for TCP/UDP) and it’s maintained by the NGINX team itself. It first appeared back in 2017 and has been quietly maturing ever since — it’s not an after-thought, it’s a first-class module shipped and supported alongside NGINX. Think of it as the web server growing a small, fast brain you can program in a language you probably already know.

Why Would You Want JavaScript in Your Web Server?

Fair question. Here’s where NJS genuinely shines:

  • Token validation at the edge — check JWT tokens before the request hits your application server. Invalid token? Rejected at NGINX, backend never sees it.
  • Custom routing logic — canary deployments, A/B testing, user-based routing. Route 10% of traffic to your new version based on user ID hash.
  • Header manipulation — add security headers, strip internal headers, inject request IDs.
  • Request transformation — modify the request body or headers before proxying upstream.
  • Dynamic responses — generate JSON responses from request data without hitting the backend at all.
  • IP geolocation logic — combine with the GeoIP module to make routing decisions based on country.

The key advantage: all this runs in NGINX, before your backend is involved. For things like auth checks and routing decisions, moving logic to the edge can eliminate entire round-trips.

Learning NJS: A Sane Path From Zero

If you already know a little JavaScript, NJS is one of the friendlier things you can learn in the NGINX world. Here’s the order I’d recommend so you don’t faceplant:

  1. Confirm the module is there (the next section) — five seconds, saves you an hour of confusion.
  2. Write Hello World with js_content. Get one endpoint returning text. Celebrate.
  3. Learn the request object rr.uri, r.args, r.headersIn, r.headersOut, r.return(), r.internalRedirect(). This is 80% of everything you’ll ever do.
  4. Learn js_set — compute an NGINX variable from a JS function. This is how NJS plugs into the rest of your config.
  5. Add logging with ngx.log() and learn to read the error log. You cannot debug what you cannot see.
  6. Build one real thing — edge auth or canary routing (both below). Real projects teach faster than tutorials.
  7. Read the official NJS reference when you hit a wall — it’s terse but accurate, and the example repo is gold.

Budget a weekend to get genuinely comfortable. The syntax is familiar; the mental model shift (“this runs inside the server, synchronously, on every request”) is the part that takes a beat.

Check If NJS Is Available

First things first — verify your NGINX build includes the NJS module:

nginx -V 2>&1 | grep -o with-http_js_module
# or check it loads as a dynamic module:
ls /usr/lib/nginx/modules/ | grep js

If you installed NGINX from the official repos or from our packages at deb.myguard.nl (or the daily-rebuilt Docker images), NJS is included. If you compiled NGINX yourself and didn’t add --with-http_js_module, you’ll need to recompile or load it as a dynamic module with load_module.

Your First NJS Script (Hello World)

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

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

export default { hello };

Reference it in your NGINX config:

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

server {
    listen 80;

    location /hello {
        js_content main.hello;
    }
}

Reload NGINX and test it:

nginx -t && nginx -s reload
curl http://localhost/hello
# Output: Hello from NJS!

The r parameter is the request object — your gateway to everything: headers, query parameters, the client IP, the URI, all of it.

Working with Requests: Headers, Query Params, Variables

function process_request(r) {
    // Read query parameters
    var user_id = r.args.user_id;

    // Read incoming headers
    var auth_token = r.headersIn['Authorization'];

    // Set response headers
    r.headersOut['X-Processed-By'] = 'NJS';
    r.headersOut['X-Timestamp'] = Date.now().toString();

    if (!auth_token) {
        r.return(401, JSON.stringify({ error: 'Missing Authorization' }));
        return;
    }

    r.return(200, 'User ' + user_id + ' authenticated');
}

export default { process_request };

Real Use Case: Edge Authentication

Validate Bearer tokens at the edge, before the request ever reaches your application:

function authenticate(r) {
    var token = r.headersIn['Authorization'];

    if (!token || !token.startsWith('Bearer ')) {
        r.return(401, JSON.stringify({ error: 'Missing or invalid token' }));
        return;
    }

    var token_value = token.substring(7);

    if (token_value.length < 32) {
        r.return(401, JSON.stringify({ error: 'Token too short' }));
        return;
    }

    // Token looks OK, pass to backend
    r.internalRedirect('@backend');
}

export default { authenticate };

In NGINX config:

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

server {
    location /api/ {
        js_content auth.authenticate;
    }

    location @backend {
        proxy_pass http://app_server;
    }
}

Real Use Case: Canary Deployments

Route 10% of traffic to your new version based on a consistent hash of the user ID:

function canary_route(r) {
    var user_id = r.args.user_id || r.headersIn['X-User-ID'] || '0';
    var canary_percentage = 10;

    // Hash the user ID to a consistent bucket
    var bucket = user_id.charCodeAt(0) % 100;

    if (bucket < canary_percentage) {
        r.internalRedirect('@canary_backend');
    } else {
        r.internalRedirect('@stable_backend');
    }
}

export default { canary_route };

Same user always goes to the same version — no random flipping between new and old within a session.

Real Use Case: Dynamic JSON API at the Edge

Generate a JSON response from request data — no backend involved:

function echo_request(r) {
    var response = {
        path: r.uri,
        method: r.method,
        query: r.args,
        timestamp: Date.now(),
        client_ip: r.remoteAddress
    };

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

export default { echo_request };
NJS vs Lua comparison for NGINX and Angie request scripting

NJS Pros and Cons: The Honest Scorecard

Every tool is a trade-off. Here’s the unvarnished version so you can decide whether NJS fits your problem before you build half a system on it.

The Pros

  • Familiar language — it’s JavaScript. Your whole team can read and write it on day one, no new language to learn.
  • Zero extra infrastructure — no Node.js process, no sidecar, no microservice to deploy, monitor, or pay for. It runs inside NGINX.
  • Microsecond latency — in-process means no network hop. Auth and routing decisions happen before your backend even wakes up.
  • First-party and maintained — built and shipped by the NGINX team, with TypeScript type definitions and an official examples repo.
  • Works on Angie too — the same module, unchanged scripts, on the Angie fork.
  • Great for “glue” logic — header rewriting, request shaping, token pre-checks, dynamic config values via js_set.

The Cons

  • Synchronous by nature — a slow script blocks the worker. There are async primitives (fetch(), js_periodic) in modern NJS, but the mental model is “be fast or get out of the way.”
  • No npm ecosystem — you can’t npm install your favourite library. You get NJS’s own standard library and that’s it.
  • Smaller community than Lua — fewer Stack Overflow answers, fewer battle-tested recipes. The official docs are your main companion.
  • Limited standard library — JSON, Date, Math, RegExp, Buffer, crypto are there; full ES2022+ and Node APIs are not.
  • Not for heavy computation or stateful work — no direct database connections, no long-running jobs. That logic belongs in your application.
  • Debugging is log-driven — no step debugger. You lean on ngx.log() and the error log.

NJS vs Lua: Which Scripting Module Should You Use?

NJS isn’t the only way to script NGINX — the OpenResty Lua modules are the other big option, and people ask about this constantly. The short version:

  • Pick NJS if your team knows JavaScript, your logic is request-shaping / auth / routing, and you want a first-party module with minimal setup.
  • Pick Lua / OpenResty if you need rich async I/O — talking to Redis, MySQL, or external APIs during a request — or a mature ecosystem of ready-made libraries (lua-resty-*) for rate limiting, sessions, OpenID Connect, and more.
  • Use both — they coexist fine. NJS for lightweight glue, Lua where you need cosocket networking and the OpenResty toolbox.

If you think you’ll need database or external-API calls mid-request, read the OpenResty Lua modules guide before committing to NJS — that’s the single biggest deciding factor.

Debugging NJS Code

NJS has its own logging function that writes to the NGINX error log:

function debug_handler(r) {
    r.log('Request URI: ' + r.uri);
    r.log('Headers: ' + JSON.stringify(r.headersIn));
    r.log('Args: ' + JSON.stringify(r.args));
    r.return(200, 'Check error log');
}

export default { debug_handler };

Then tail the error log to see your output:

tail -f /var/log/nginx/error.log | grep "js:"

For local development, the standalone njs CLI (a REPL, like the node prompt) lets you test pure logic without reloading NGINX at all. Install it from our repo and run njs to get an interactive prompt.

What NJS Can’t Do (The Honest Bit)

NJS is powerful, but it’s not a full JavaScript runtime. Know the limitations before you go too deep:

  • Synchronous-first — the request handler model is synchronous. Long-running work blocks the worker. Keep handlers fast (sub-millisecond ideally).
  • Limited networking — modern NJS has ngx.fetch() for outbound HTTP in some contexts, but for external auth the classic, safer pattern is NGINX’s own auth_request.
  • No npm modules — you can’t import third-party packages. NJS has its own standard library, which is good but limited.
  • No DOM, no browser APIs — it’s server-side only. No window, no document.
  • Limited standard library — most common JS APIs are there (JSON, Date, Math, RegExp, Buffer, crypto), but not all ES2022+ features.
  • No direct database connections — NJS can’t open a MySQL or Redis socket itself. Use it for logic that doesn’t need stateful external I/O, or reach for Lua/OpenResty instead.

Frequently Asked Questions

Is NJS the same as Node.js?

No — completely different project. Node.js uses the V8 JavaScript engine and is a full runtime for building server applications. NJS is a separate, lightweight engine designed specifically for NGINX scripting. They share JavaScript syntax (mostly ES5/6) but no runtime, no modules, no compatibility. Don’t try to use Node.js packages in NJS.

Is NJS hard to learn?

If you know basic JavaScript, no. The syntax is familiar and the API surface is small — the request object r plus a handful of directives covers most real work. Budget a weekend to get comfortable. The trickiest part isn’t the language; it’s the mental shift to “this runs synchronously, inside the server, on every request.”

NJS vs Lua — which is better for NGINX?

Neither is universally “better.” NJS wins on familiarity and zero setup for request-shaping and auth glue. Lua/OpenResty wins when you need rich async I/O (Redis, MySQL, external APIs) mid-request and a mature library ecosystem. Many setups use both. See our OpenResty Lua modules guide for the other side of the coin.

Does NJS slow down NGINX?

Only if your scripts are slow. NJS runs synchronously in the NGINX worker process, so a script that takes 10ms will block that worker for 10ms. Keep scripts fast and simple — header checks, token validation, routing decisions. These typically run in microseconds. Heavy computation belongs in your application, not NJS.

Does Angie support NJS too?

Yes. Angie (the NGINX fork with extended features) ships the same NJS module. If you migrate from NGINX to Angie, your NJS scripts work without modification.

Can I use TypeScript with NJS?

You can write in TypeScript and transpile to plain JavaScript before deploying to NGINX. The NGINX team publishes TypeScript type definitions for the NJS API. For complex NJS projects, TypeScript is worth the extra build step.

Where do NJS scripts live?

Anywhere on the filesystem NGINX can read — typically /etc/nginx/njs/. Reference them with js_import in your NGINX config. Keep them in version control alongside your NGINX config; they’re just as important.

Related Posts