Using JavaScript (njs) in NGINX for Custom Middleware
Goal: Learn how to use the official NGINX JavaScript module (
njs) to build custom middleware — for logic like authentication, geofencing, or header inspection — without needing Node.js, Lua, or rebuilding NGINX.
Overview
Starting with NGINX 1.19+, you can extend NGINX behavior using njs, a lightweight JavaScript engine embedded directly into NGINX.
It allows you to write custom logic that runs inside the web server process — similar to middleware in Express.js — without proxies or microservices.
You can use it for:
- Dynamic access control (auth, API keys, JWT)
- Geofencing or IP-based blocking
- Intelligent request routing
- Custom logging or header rewriting
- Edge-layer logic before proxying to backend servers
1. Prerequisites
You need:
- NGINX ≥ 1.19 (dynamic modules supported)
- Root or sudo access
Check version and modules:
nginx -V
If you see these files on your system:
/usr/lib/nginx/modules/ngx_http_js_module.so
/usr/lib/nginx/modules/ngx_stream_js_module.so
then njs is already available.
2. Enable the JavaScript Module
Edit /etc/nginx/nginx.conf and load both modules at the top:
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
load_module /usr/lib/nginx/modules/ngx_stream_js_module.so;
Test and reload:
nginx -t && systemctl reload nginx
3. Create a JavaScript Middleware Directory
Create a dedicated folder:
mkdir -p /etc/nginx/njs
You can place all your middleware logic here:
/etc/nginx/njs/
├── auth.js
├── geofence.js
└── logger.js
4. Write Your First Middleware (Example: Geofence)
Create /etc/nginx/njs/geofence.js:
export default {
check: function (r) {
const country = (r.headersIn['CF-IPCountry'] || 'UNKNOWN').trim();
const lat = parseFloat(r.headersIn['CF-IPLatitude'] || 0);
const lon = parseFloat(r.headersIn['CF-IPLongitude'] || 0);
r.log(`\ud83c\udf0d GeoCheck: country=${country}, lat=${lat}, lon=${lon}`);
// Simple example bounds (Los Baños area)
const bounds = { minLat: 14.162, maxLat: 14.179, minLon: 121.214, maxLon: 121.238 };
if (country !== 'PH') {
r.return(403, `Access denied. Country ${country} not allowed.\n`);
return;
}
if (
lat && lon &&
(lat < bounds.minLat || lat > bounds.maxLat ||
lon < bounds.minLon || lon > bounds.maxLon)
) {
r.return(403, 'Access denied. Outside barangay boundary.\n');
return;
}
r.return(200, '\u2705 Access granted. Inside barangay zone!\n');
}
};
This script:
- Reads Cloudflare geolocation headers
- Logs incoming requests
- Denies access if the request comes from outside the Philippines or a defined latitude/longitude range
5. Import and Run It in NGINX
Create /etc/nginx/sites-enabled/njs-test.conf:
js_import geo from /etc/nginx/njs/geofence.js;
server {
listen 9020;
server_name _;
location / {
# Run JS as content handler
js_content geo.check;
}
}
Reload:
nginx -t && systemctl restart nginx
6. Test the Middleware
curl -H "CF-IPCountry: PH" \
-H "CF-IPLatitude: 14.170" \
-H "CF-IPLongitude: 121.225" \
http://localhost:9020/
Output:
Access granted. Inside barangay zone!
Test outside the area:
curl -H "CF-IPCountry: PH" -H "CF-IPLatitude: 14.190" -H "CF-IPLongitude: 121.250" http://localhost:9020/
Output:
Access denied. Outside barangay boundary.
7. Passing Variables to JavaScript Middleware
You can make your middleware flexible by passing variables from NGINX to JavaScript using query parameters, headers, or NGINX variables.
a) Via Query Parameters
curl "http://localhost:9020/?zone=barangay1"
In JS:
const zone = r.args.zone || 'unknown';
r.return(200, `Requested zone: ${zone}\n`);
b) Via NGINX Variables
location /barangay1 {
set $zone_name "Barangay 1";
js_content geo.check;
}
In JS:
const zone = r.variables.zone_name;
r.return(200, `Serving zone: ${zone}\n`);
c) Via Request Headers
curl -H "X-Zone: Barangay 2" http://localhost:9020/
In JS:
const zone = r.headersIn['X-Zone'] || 'unknown';
8. Dynamic Parameter-Based Routing
You can even extract variables directly from the URI:
location ~ ^/geo/(.*) {
set $zone $1;
js_content geo.dynamic;
}
In JS:
export default {
dynamic: function (r) {
const zone = r.variables.zone;
r.return(200, `Dynamic zone route: ${zone}\n`);
}
};
Example:
curl http://localhost:9020/geo/barangay5
Output:
Dynamic zone route: barangay5
9. Debugging and Logging
Check runtime logs:
tail -f /var/log/nginx/error.log
Inside your JS, you can log:
r.log(`Geo middleware triggered for ${r.remoteAddress}`);
10. Summary
| Step | Action |
|---|---|
| 1 | Enable ngx_http_js_module |
| 2 | Create /etc/nginx/njs/ for scripts |
| 3 | Write JS middleware with exported functions |
| 4 | Import via js_import |
| 5 | Run using js_content |
| 6 | Pass data via headers, variables, or query params |
| 7 | Log and debug in /var/log/nginx/error.log |
Example File Layout
/etc/nginx/
├── nginx.conf
├── njs/
│ ├── auth.js
│ ├── geofence.js
│ ├── logger.js
│ └── main.js
└── sites-enabled/
└── njs-test.conf
Next Steps
- Build middleware for JWT or API key authentication
- Implement geo-blocking with a database or GeoJSON polygon
- Create a reusable JS router to load barangay boundary data dynamically
- Log access results to external services or files
Author: Marcuwynu DevOps Team
Version: 1.1
Last Updated: October 2025