ngs

Write functional, type-safe nginx scripts (njs) with Gleam

Package Version Hex Docs

ngs is a Gleam library that provides type-safe bindings to Nginx JavaScript (njs) runtime, allowing you to write functional, type-safe nginx handlers in Gleam instead of plain JavaScript.

Table of Contents

Overview

ngs bridges the gap between Gleam’s type-safe functional programming and Nginx’s powerful JavaScript scripting capabilities (njs). It enables you to:

The compiled JavaScript conforms to the es2020 standard and requires NJS with QuickJS engine support.

Features

Installation

Add ngs to your Gleam project

gleam add ngs

Set up npm dependencies

npm install

Requirements

Quick Start

1. Create a simple nginx handler

Create a Gleam module for your handler:

// src/my_handler.gleam
import njs/http.{type HTTPRequest}
import njs/ngx.{type JsObject}

fn hello(r: HTTPRequest) -> Nil {
  r
  |> http.return_text(200, "Hello from Gleam!\n")
}

pub fn exports() -> JsObject {
  ngx.object()
  |> ngx.merge("hello", hello)
}

2. Build the application

npm run build

This compiles your Gleam code to JavaScript and bundles it with esbuild.

3. Configure nginx

Create an nginx configuration that loads your handler:

daemon off;
error_log logs/error.log debug;
pid logs/nginx.pid;

events {
    worker_connections 64;
}

http {
    js_engine qjs;
    js_path "njs/";

    js_import main from app.js;

    server {
        listen 8888;

        location / {
            js_content main.hello;
        }
    }
}

4. Run nginx

nginx -c path/to/nginx.conf -p path/to/runtime

Your handler is now running at http://localhost:8888/!

API Documentation

ngs provides comprehensive bindings to njs APIs organized by module:

Core HTTP Module (njs/http)

Type-safe bindings for HTTP request handling:

import njs/http

// Request information
http.headers_in(request)
http.method(request)
http.uri(request)
http.remote_address(request)

// Response handling
http.return_text(request, 200, "Hello")
http.return_code(request, 200)
http.set_status(request, 201)
http.set_headers_out(request, "Content-Type", "application/json")

// Async operations
http.subrequest(request, "/api", options)

Key types:

Stream Module (njs/stream)

Handle TCP/UDP streams in nginx:

import njs/stream

// Session control
stream.allow(session)
stream.decline(session)
stream.deny(session)

// Event handling
stream.on(session, "data", callback)

// Logging
stream.log(session, "Connection established")

Crypto Module (njs/crypto)

Modern cryptographic operations:

import njs/crypto

// Hashing
let hash = crypto.create_hash("sha256")
  |> crypto.hash_update("data")
  |> crypto.hash_digest(Hex)

// HMAC
let hmac = crypto.create_hmac("sha256", "secret")
  |> crypto.hmac_update("data")
  |> crypto.hmac_digest(Base64)

// AES encryption
crypto.encrypt(AesGcm(...), key, plaintext)
crypto.decrypt(AesGcm(...), key, ciphertext)

Supports:

Synchronous hash/HMAC bindings use njs’s built-in crypto module through an ESM import. Async operations use the global crypto.subtle object. QuickJS supports both APIs, but has no globalThis.require; preserve the native import by marking crypto external in esbuild (see the bundling instructions below).

Buffer Module (njs/buffer)

Efficient binary data handling:

import njs/buffer

// Create buffers
let buf = buffer.alloc(1024)

// Read/write operations
buffer.write_int32_be(buf, 42, 0)
let value = buffer.read_uint32_be(buf, 0)

// String encoding/decoding
buffer.from_string("hello", Utf8)
buffer.to_string(buf, Utf8, 0, 5)

File System Module (njs/fs)

Synchronous and asynchronous file operations:

import njs/fs

// Sync operations
fs.stat_sync("file.txt")
fs.read_file_sync("data.json", Utf8)
fs.write_file_sync("output.txt", "data", Utf8)

// Async operations
use handle <- promise.await(fs.promises_open("file.txt", "r", 0o644))
use result <- promise.await(fs.file_handle_read(handle, buf, 0, 1024, None))

Utility Modules

See hexdocs.pm/ngs for complete API documentation.

Examples

The project includes 25+ example nginx handlers organized by category:

HTTP Handlers

ExampleDescription
http_helloBasic “Hello World” handler
http_decode_uriURI decoding with query parameters
http_complex_redirectsComplex redirect logic
http_join_subrequestsJoin multiple subrequests
http_subrequests_chainingChain subrequests sequentially

Authorization

ExampleDescription
http_authorization_jwtJWT verification
http_authorization_gen_hs_jwtGenerate HS256 JWT tokens
http_authorization_auth_requestAuth request pattern
http_authorization_request_bodyValidate request body
http_authorization_secure_link_hashSecure link with hash

Certificate Handling

ExampleDescription
http_certs_dynamicDynamic certificate loading
http_certs_fetch_httpsFetch certificates via HTTPS
http_certs_subject_alternativeX.509 subject alternative name parsing

Response Modification

ExampleDescription
http_response_to_lower_caseConvert response to lowercase
http_response_modify_set_cookieModify Set-Cookie headers

Async Operations

ExampleDescription
http_async_var_auth_requestAsync auth request
http_async_var_js_header_filterAsync header filter

Utilities

ExampleDescription
http_logging_num_requestsRequest counting
http_rate_limit_simpleSimple rate limiting
http_api_set_keyvalKey-value API
misc_file_ioFile I/O operations
misc_aes_gcmAES-GCM encryption

Stream Handlers

ExampleDescription
stream_auth_requestStream authentication
stream_detect_httpHTTP detection in streams
stream_inject_headerInject headers into streams

Each example includes:

Testing

ngs uses Bun for integration testing with real nginx instances.

Run all tests

bun test

Run specific test suite

bun test tests/hello/do.test.js

Crypto regression tests execute the Gleam bindings in nginx with js_engine qjs and check that both build and watch bundles exclude a conflicting crypto package:

gleam test
bun test tests/misc_crypto

Test infrastructure

The test harness (tests/harness.js) provides:

Test structure

import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { startNginx, stopNginx, cleanupRuntime, TEST_URL } from "../harness.js";

const MODULE = "http_hello";

describe("http hello", () => {
  beforeAll(async () => {
    await startNginx(`dist/${MODULE}/nginx.conf`, MODULE);
  });

  afterAll(async () => {
    await stopNginx();
    cleanupRuntime(MODULE);
  });

  test("outputs 'hello' text", async () => {
    const res = await fetch(`${TEST_URL}/hello`);
    expect(res.status).toBe(200);
    const body = await res.text();
    expect(body).toBe("Hello World!\n");
  });
});

Development

Build system

ngs uses a custom build system (src/ngs.gleam) that:

  1. Compiles Gleam code to JavaScript
  2. Bundles applications with esbuild
  3. Copies nginx configurations to dist directory

Bundling ngs from another bundler (Bun, webpack, rollup, vite, …)

njs is QuickJS, not Node and not a browser. ngs’s own npm run build is already configured correctly (esbuild, target: es2020, njs builtins marked external). But if you consume ngs as a Gleam dependency and bundle the result with a different tool, you must replicate two precautions, or the bundler will silently corrupt your output.

1. Do not use a “browser” target. Browser targets inject polyfills that shadow njs globals. For example, Bun’s target: "browser" replaces the global Buffer with an inlined node:buffer shim whose less-common methods literally throw, and replaces import qs from 'querystring' with a wrong inlined polyfill. None of this errors at build time — it only breaks at runtime. Use a node target, which leaves Buffer/process as runtime globals and emits builtins as bare specifiers ('crypto', not 'node:crypto') that njs resolves to its own modules.

2. Mark njs builtin modules external so the bundler never tries to resolve or polyfill them:

querystring  crypto  fs  xml  zlib  buffer

xml is njs-only (Node has no such builtin), so it must be listed explicitly even under a node target, or resolution fails. process is an njs global, not an importable module — never write import ... from 'process'; use the global (as ngs’s bindings do).

esbuild (what ngs uses internally — see src/ngs_ffi.mjs):

build({
  entryPoints: [entry], bundle: true, format: "esm", outfile: out,
  target: ["es2020"],                                          // syntax compatibility
  external: ["querystring", "crypto", "fs", "xml", "zlib", "buffer"],
});

For esbuild, target controls JavaScript syntax, not package resolution. The explicit external list keeps imports such as import nativeCrypto from "crypto" in the output for njs to resolve. Removing crypto from that list can bundle an unrelated package with the same name. Do not replace the import with require("crypto"): esbuild’s ESM require helper cannot load the module in QuickJS.

Bun.build:

await Bun.build({
  entrypoints: [entry], format: "esm", outdir,
  target: "node",                                              // NOT "browser"
  external: ["querystring", "crypto", "fs", "xml", "zlib", "buffer"],
});

Verify the output — these greps must both come back empty:

grep -E "node:buffer browser polyfill|from ['\"]node:" dist/**/app.js   # injected polyfills
grep -E "from ['\"]process['\"]"                       dist/**/app.js   # process imported as a module

Available npm scripts

# Build all applications
npm run build

# Watch mode with live reloading
npm run watch

# Clean dist directory (keeps Gleam cache)
npm run clean

# Full clean including Gleam build cache
npm run purge

Adding a new example

  1. Create the handler module
mkdir -p src/app/my_example
// src/app/my_example/my_example.gleam
import njs/http.{type HTTPRequest}
import njs/ngx.{type JsObject}

fn handler(r: HTTPRequest) -> Nil {
  r |> http.return_text(200, "OK")
}

pub fn exports() -> JsObject {
  ngx.object() |> ngx.merge("handler", handler)
}
  1. Create nginx configuration
# src/app/my_example/nginx.conf
daemon off;
error_log logs/error.log debug;
pid logs/nginx.pid;

events {
    worker_connections 64;
}

http {
    js_engine qjs;
    js_path "njs/";

    js_import main from app.js;

    server {
        listen 8888;

        location / {
            js_content main.handler;
        }
    }
}
  1. Register in build system

Add your app to the apps() list in src/ngs.gleam:

App(
  "my_example",
  "./build/dev/javascript/ngs/app/my_example/my_example.mjs",
),
  1. Build and test
npm run build
  1. Create tests (optional)
mkdir -p tests/my_example
// tests/my_example/do.test.js
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { startNginx, stopNginx, cleanupRuntime, TEST_URL } from "../harness.js";

const MODULE = "my_example";

describe("my example", () => {
  beforeAll(async () => {
    await startNginx(`dist/${MODULE}/nginx.conf`, MODULE);
  });

  afterAll(async () => {
    await stopNginx();
    cleanupRuntime(MODULE);
  });

  test("works correctly", async () => {
    const res = await fetch(`${TEST_URL}/`);
    expect(res.status).toBe(200);
  });
});

Project Structure

ngs/
├── src/
│   ├── njs/                 # njs API bindings (19 modules)
│   │   ├── http.gleam       # HTTP request handling
│   │   ├── stream.gleam     # TCP/UDP stream handling
│   │   ├── crypto.gleam     # Cryptographic operations
│   │   ├── buffer.gleam     # Binary data manipulation
│   │   ├── fs.gleam         # File system operations
│   │   ├── ngx.gleam        # Core nginx utilities
│   │   ├── headers.gleam    # HTTP headers
│   │   ├── console.gleam    # Console logging
│   │   ├── timers.gleam     # Timer operations
│   │   ├── querystring.gleam # URL query parsing
│   │   ├── xml.gleam        # XML parsing
│   │   ├── zlib.gleam       # Compression
│   │   ├── process.gleam     # Process information
│   │   ├── text_encoder.gleam # Text encoding
│   │   ├── text_decoder.gleam # Text decoding
│   │   └── shared_dict.gleam # Shared memory
│   ├── app/                 # Example handlers (25+)
│   │   ├── http_hello/
│   │   │   ├── http_hello.gleam
│   │   │   └── nginx.conf
│   │   ├── http_authorization_jwt/
│   │   ├── http_rate_limit_simple/
│   │   └── ...
│   └── ngs.gleam           # Build system
├── tests/
│   ├── harness.js          # Test infrastructure
│   ├── preload.js          # Test preloader (builds apps)
│   ├── mocks/              # Mock servers
│   │   ├── redis.js
│   │   ├── postgres.js
│   │   ├── consul.js
│   │   ├── oidc.js
│   │   ├── acme.js
│   │   └── http.js
│   ├── hello/
│   │   └── do.test.js
│   └── ...
├── dist/                   # Build output (generated)
├── build/                  # Gleam build artifacts (generated)
├── gleam.toml             # Gleam project configuration
├── package.json           # npm scripts and dependencies
└── README.md              # This file

Further Documentation

Complete API documentation is available at hexdocs.pm/ngs.

For njs documentation, see nginx.org/en/docs/njs/.

License

Apache-2.0 - see LICENSE for details.

Search Document