ngs
Write functional, type-safe nginx scripts (njs) with Gleam
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
- Features
- Installation
- Requirements
- Quick Start
- API Documentation
- Examples
- Testing
- Development
- Project Structure
Overview
ngs bridges the gap between Gleam’s type-safe functional programming and Nginx’s powerful JavaScript scripting capabilities (njs). It enables you to:
- Write nginx handlers using Gleam’s expressive type system
- Leverage njs’s extensive API with compile-time safety
- Offload HTTP server logic to nginx for better performance
- Use Gleam’s pattern matching and functional composition for request handling
The compiled JavaScript conforms to the es2020 standard and requires NJS with QuickJS engine support.
Features
- Type-safe njs bindings - Full coverage of njs HTTP, Stream, and utility APIs
- Functional programming - Use Gleam’s pattern matching and functional composition
- Build system integration - Automated compilation and bundling with esbuild
- Watch mode - Live reloading during development
- Comprehensive examples - 25+ real-world nginx handler implementations (to be finished by AI)
- Integration testing - Bun-based tests with real nginx instances
- Mock infrastructure - Built-in mock servers for Redis, Postgres, Consul, OIDC, ACME, and HTTP
Installation
Add ngs to your Gleam project
gleam add ngs
Set up npm dependencies
npm install
Requirements
- Gleam >= 1.14.0
- NJS with QuickJS engine (ES2020 support)
- Node.js (for esbuild bundling)
- Bun (for running tests)
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:
HTTPRequest- Incoming HTTP requestHTTPResponse- HTTP response (alias of HTTPRequest)HTTPHandler- Handler function type
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:
- SHA-256, SHA-384, SHA-512
- HMAC with various algorithms
- AES-CTR, AES-CBC, AES-GCM encryption
- RSA-OAEP
- Key generation, import/export
- Key derivation
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
njs/ngx- Core nginx utilities, logging, shared dictionarynjs/headers- HTTP header manipulationnjs/console- Console loggingnjs/timers- Timer operationsnjs/querystring- URL query parsing/formattingnjs/xml- XML parsingnjs/zlib- Compressionnjs/process- Process informationnjs/text_encoder- Text encodingnjs/text_decoder- Text decodingnjs/shared_dict- Shared memory dictionaries
See hexdocs.pm/ngs for complete API documentation.
Examples
The project includes 25+ example nginx handlers organized by category:
HTTP Handlers
| Example | Description |
|---|---|
http_hello | Basic “Hello World” handler |
http_decode_uri | URI decoding with query parameters |
http_complex_redirects | Complex redirect logic |
http_join_subrequests | Join multiple subrequests |
http_subrequests_chaining | Chain subrequests sequentially |
Authorization
| Example | Description |
|---|---|
http_authorization_jwt | JWT verification |
http_authorization_gen_hs_jwt | Generate HS256 JWT tokens |
http_authorization_auth_request | Auth request pattern |
http_authorization_request_body | Validate request body |
http_authorization_secure_link_hash | Secure link with hash |
Certificate Handling
| Example | Description |
|---|---|
http_certs_dynamic | Dynamic certificate loading |
http_certs_fetch_https | Fetch certificates via HTTPS |
http_certs_subject_alternative | X.509 subject alternative name parsing |
Response Modification
| Example | Description |
|---|---|
http_response_to_lower_case | Convert response to lowercase |
http_response_modify_set_cookie | Modify Set-Cookie headers |
Async Operations
| Example | Description |
|---|---|
http_async_var_auth_request | Async auth request |
http_async_var_js_header_filter | Async header filter |
Utilities
| Example | Description |
|---|---|
http_logging_num_requests | Request counting |
http_rate_limit_simple | Simple rate limiting |
http_api_set_keyval | Key-value API |
misc_file_io | File I/O operations |
misc_aes_gcm | AES-GCM encryption |
Stream Handlers
| Example | Description |
|---|---|
stream_auth_request | Stream authentication |
stream_detect_http | HTTP detection in streams |
stream_inject_header | Inject headers into streams |
Each example includes:
- Gleam handler implementation in
src/app/<name>/ - Nginx configuration in
src/app/<name>/nginx.conf - Integration tests in
tests/<name>/
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:
- nginx process management - Start/stop nginx with custom configs
- Isolated runtime directories - Clean runtime environment for each test
- Mock servers - Pre-configured mock servers for external dependencies:
- Redis (port 16379)
- PostgreSQL (port 15432)
- Consul (port 18500)
- OIDC (port 19000)
- ACME (port 14000)
- HTTP backends (ports 19001-19003)
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:
- Compiles Gleam code to JavaScript
- Bundles applications with esbuild
- 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 buildis already configured correctly (esbuild,target: es2020, njs builtins markedexternal). 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
- 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)
}
- 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;
}
}
}
- 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",
),
- Build and test
npm run build
- 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.