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.
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, 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/ES6 JavaScript, 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 and it’s maintained by the NGINX team. It’s not an after-thought; it’s a first-class module.
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.
Check If NJS Is Available
First things first — verify your NGINX build includes the NJS module:
nginx -T 2>&1 | grep njs
# or
nginx -V 2>&1 | grep js
If you installed NGINX from the official repos or from our packages at deb.myguard.nl, NJS is included. If you compiled NGINX yourself and didn’t add --with-http_js_module, you’ll need to recompile.
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 { http: { hello } };
Reference it in your NGINX config:
js_import /etc/nginx/njs/hello.js;
server {
listen 80;
location /hello {
js_content hello;
}
}
Reload NGINX and test it:
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 { http: { 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 { http: { authenticate } };
In NGINX config:
js_import /etc/nginx/njs/auth.js;
server {
location /api/ {
js_content 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 { http: { 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 { http: { echo_request } };
Debugging NJS Code
NJS has its own logging function that writes to the NGINX error log:
function debug_handler(r) {
ngx.log(ngx.ERR, 'Request URI: ' + r.uri);
ngx.log(ngx.ERR, 'Headers: ' + JSON.stringify(r.headersIn));
ngx.log(ngx.ERR, 'Args: ' + JSON.stringify(r.args));
r.return(200, 'Check error log');
}
export default { http: { debug_handler } };
Then tail the error log to see your output:
tail -f /var/log/nginx/error.log | grep "js:"
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:
- No async/await — NJS is synchronous. Long-running operations block the entire worker process. Keep your scripts fast (under 1ms ideally).
- No external HTTP calls — you can’t fetch a URL inside NJS. If you need to call an external API for auth, use
auth_requestat the NGINX level instead. - 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, nodocument. - Limited standard library — most common JS APIs are there (JSON, Date, Math, RegExp, Buffer), but not all ES2022+ features.
- No direct database connections — NJS can’t connect to MySQL or Redis directly. Use it for logic that doesn’t need external I/O.
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.
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) supports the same NJS module. If you’ve migrated from NGINX to Angie, your NJS scripts work without modification.
How do I pass data between NJS and NGINX variables?
Use js_set to compute an NGINX variable from a JavaScript function, and r.variables to read/write NGINX variables from within NJS. This is the bridge between the JavaScript world and the NGINX configuration world.
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
- Angie vs NGINX: What’s Actually Different? — Angie supports NJS and adds even more scripting capabilities
- Migrating from NGINX to Angie — Your NJS scripts work unchanged on Angie
- NGINX Lua Module Guide — Lua is the other embedded scripting option for NGINX
- NGINX Performance Optimization Guide — Fine-tune NGINX beyond scripting