NGINX is a free open source web server that is used as a high performance reverse proxy, load balancer and more. Since its public release in 2004 it has powered most of the worlds websites.
Have you ever wanted to extend NGINX with custom logic, like authentication or caching rules. Extending the web server can be done using Lua, C or NJS.
Why extend NGINX.
- Custom logic and behavior
- Performance optimization like load balancing and caching
- Security Enhancement eg through rate limiting
In this guide, I'll do the following:
- Run JavaScript inside NGINX using the NJS module
- Create a basic API gateway
- Add authentication and custom headers
NJS SCRIPTING
JS support for NGINX is through the NJS module, which was first introduced in 2016. There are two js modules: ngx_http_js_module and ngx_stream_js_module. On this article I will be using the ngx_http_js_module.
I will be building a basic/primitive api gateway using nginx, with no auth server. Data will be on the web server, just for learning purposes.
Prerequisites
- Basic NGINX knowledge
-
NGINX and njs => 0.9 installed
- Check versions and install if not installed:
nginx -v njs -v
BASIC NGINX CONFIG
The following is a basic NGINX config file that returns hello world:
This is config listens to port 8080 and returns status 200 and hello world.
# /etc/nginx/nginx.conf
events {}
http {
server {
listen 8080;
server name _;
location / {
return 200 "hello world"
}
}
}
Then run
sudo nginx -t #to test the file
sudo systemctl reload nginx
USING NJS
The NJS module has several directives that can be used in the config file. These help NGINX deal with JS files eg importing, reading etc.
Let's look at these:
Importing and response
-
js_path: used to set the path for the njs files -
js_import: imports the file - and
js_content: used to set content from a js function to the response
Let's create our test js file
// /etc/nginx/njs/hello.js
function sayHello(r) {
r.return(200, "hello there");
}
export default { sayHello };
Importing and using it in the config:
# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;
events {}
http {
js_path /etc/nginx/njs/; #sets the path
js_import hello from hello.js; #imports file as hello
server {
listen 8080;
server_name _;
location / {
js_content hello.sayHello; #sets content response
}
}
}
Test the file and reload the server.
When we make a request to the server:
> curl localhost:8080
hello there
Adding a Proxy
Instead of just sending hello world, lets proxy to jsonplaceholder.typecode.com
So lets create a new path for this:
# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;
#include /etc/nginx/modules-enabled/*.conf;
events {}
http {
js_path /etc/nginx/njs/; #sets the path
js_import hello from hello.js; #imports file as hello
server {
listen 8080;
server_name _;
location / {
js_content hello.sayHello;
}
location /jph/ {
resolver 1.1.1.1; #using Cloudflare DNS resolver
proxy_pass https://jsonplaceholder.typicode.com/;
proxy_ssl_server_name on;
}
}
}
When we make a request:
> curl localhost:8080/jph/posts/1
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Using auth_request Module
Nice!
But we do not want to give any unauthorized persons the ability to use this endpoint. It is free but I am gatekeeping.
We can use the auth_request directive from NGINX. This is used for authentication. I will use inline comments for more elaboration. (It is not a JS module though.)
# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;
events {}
http {
js_path /etc/nginx/njs/; #sets the path
js_import hello from hello.js; #imports file as hello
js_import auth from auth.js;
js_shared_dict_zone zone=apikeys:1M; #creates a share memory zone which all nginx workers can access
server {
listen 8080;
server_name _;
location / {
js_content hello.sayHello;
}
location /jph/ {
auth_request /auth; #uses location /auth to authenticate request
resolver 1.1.1.1; #using Cloudflare DNS resolver
proxy_pass https://jsonplaceholder.typicode.com/;
proxy_ssl_server_name on;
}
location = /auth {
internal; # makes the location internal to nginx only
js_content auth.doAuth; #calls the doAuth function
}
}
}
Implementing the auth.js file:
// /etc/nginx/njs/auth.js
//loading the shared object. ngx is a global nginx provided variable
const store = ngx.shared.apikeys;
//create demo user
if (!store.get("demo")) {
store.set(
"demo",
JSON.stringify({
routes: ["/api/v1/*"],
credit: 15,
name: "demo",
})
);
}
function doAuth(r) {
const key = r.headersIn["x-api-key"]; //get the key from the request header
if (!key) {
r.return(401, "missing key");
return;
}
const user = store.get(key);
if (!user) {
r.return(401, "key not found");
return;
}
r.return(200);
}
export default { doAuth };
When we make requests:
$ curl localhost:8080/jph/post/1 #without header or wrong header
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.28.0</center>
</body>
</html>
$ curl -H 'x-api-key: demo' localhost:8080/jph/posts/1 #with header
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Awesome! Now we have a shared memory zone to store our api keys. The js_shared_dict_zone is a directive that takes some options. I have use the zone=name:size option. This zone is accessible in all the JS files we create. ngx is a global object that is provided by NGINX for working with NJS. Read more on this and other variables and functions.
Creating API Keys
Well, demo cannot be the only person to use this endpoint. We can create a way for new apis to be created.
const store = ngx.shared.apikeys;
function createKey(r){
const name = r.args.name //get name from args
const key = Math.random().toString(36).substring(2, 10);
store.set(key, JSON.stringify({
credit: 15,
name: name
}))
const data = {key: key, data: JSON.parse(store.get(key))}
r.return(201, JSON.stringify(data))
}
.....
export default {doAuth , createKey}
We then add a new location endpoint:
....
server {
listen 8080;
server_name _;
location = /newkey {
js_content auth.createKey;
}
....
}
When we call this endpoint, we create a new key that will be used for authentication:
$ curl "localhost:8080/newkey?name=larry"
{"key":"205cde2b","data":{"credit":15,"name":"larry"}}
$ curl -H 'x-api-key: 205cde2b' localhost:8080/jph/posts/1 #using the key
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Modifying Response Headers with NJS”
We would like to add an id to the headers. Maybe for auditing or something else. For this, the 'js_headers_filter' will be of help. With this directive we can add, delete or change headers.
Let's add some code to the location /jph/
.....
location /jph/ {
auth_request /auth; #uses location /auth to authenticate request
js_header_filter auth.addReqId; //adds uuid to the header
resolver 1.1.1.1; #using Cloudflare DNS resolver
proxy_pass https://jsonplaceholder.typicode.com/;
proxy_ssl_server_name on;
}
.....
.........
function randomUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function addReqId(r){
r.headersOut['x-request-uuid'] = randomUUID();
}
export default {doAuth , createKey, addReqId}
When we request we get a new header added to the response:
$ curl localhost:8080/jph/posts/1 -I
HTTP/1.1 401 Unauthorized
Server: nginx/1.28.0
Date: Fri, 31 Oct 2025 17:43:03 GMT
Content-Type: text/html
Content-Length: 179
Connection: keep-alive
x-request-uuid: c4ee86fb-8799-46fc-b4f6-fd54bb5fb538
Final
These are our final files:
# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;
events {}
http {
error_log /var/log/nginx/error.log debug;
js_path /etc/nginx/njs/; #sets the path
js_import hello from hello.js; #imports file as hello
js_import auth from auth.js;
js_shared_dict_zone zone=apikeys:1M;
server {
listen 8080;
server_name _;
location = /newkey {
js_content auth.createKey;
}
location / {
js_content hello.sayHello;
}
location /jph/ {
auth_request /auth; #uses location /auth to authenticate request
js_header_filter auth.addReqId;
resolver 1.1.1.1; #using Cloudflare DNS resolver
proxy_pass https://jsonplaceholder.typicode.com/;
proxy_ssl_server_name on; #Includes the Server Name Indication (SNI)
}
location = /auth {
internal;
js_content auth.doAuth;
}
}
}
//loading the shared object. ngx is a global nginx provided variable
const store = ngx.shared.apikeys;
//create demo user
if (!store.get("demo")) {
store.set(
"demo",
JSON.stringify({
credit: 15,
name: "demo",
})
);
}
function createKey(r) {
const name = r.args.name; //get name from args
const key = Math.random().toString(36).substring(2, 10);
store.set(
key,
JSON.stringify({
credit: 15,
name: name,
})
);
const data = { key: key, data: JSON.parse(store.get(key)) };
r.return(201, JSON.stringify(data));
}
function doAuth(r) {
const key = r.headersIn["x-api-key"]; //get the key from the request header
if (!key) {
r.return(401, "missing key");
return;
}
const user = store.get(key);
if (!user) {
r.return(401, "key not found");
return;
}
r.return(200);
}
function randomUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function addReqId(r) {
r.headersOut["x-request-uuid"] = randomUUID();
}
export default { doAuth, createKey, addReqId };
Conclusion
This is obviously a simple way to use NJS. It is packed with many features that will enable you to extend your NGINX server as you wish. It is worth noting that although it gives you the NPM ecosystem, you may need to transpile the JS code so that it runs without issues.
References
Official NGINX Documentation
NJS
NJS module docsAUTH REQUEST MODULE
Docs onauth_request
Tutorials
- Fun Ways to Script NGINX using NJS A fun YouTube tutorial by NGINX channel
Top comments (0)