# http-nu [](https://github.com/cablehead/http-nu/actions/workflows/ci.yml) [](https://discord.gg/sGgYVKnk73)
The surprisingly performant, [Nushell](https://www.nushell.sh)-scriptable HTTP
server that fits in your back pocket.
<img width="600" alt="ellie" src="https://github.com/user-attachments/assets/95e70206-4db9-465a-947a-82b3ca5f4656" />
---
- [Install](#install)
- [eget](#eget)
- [cargo](#cargo)
- [NixOS](#nixos)
- [Reference](#reference)
- [GET: Hello world](#get-hello-world)
- [UNIX domain sockets](#unix-domain-sockets)
- [Reading closures from stdin](#reading-closures-from-stdin)
- [Dynamic reloads](#dynamic-reloads)
- [POST: echo](#post-echo)
- [Request metadata](#request-metadata)
- [Response metadata](#response-metadata)
- [Content-Type Inference](#content-type-inference)
- [TLS & HTTP/2 Support](#tls-support)
- [Logging](#logging)
- [Trusted Proxies](#trusted-proxies)
- [Serving Static Files](#serving-static-files)
- [Streaming responses](#streaming-responses)
- [server-sent events](#server-sent-events)
- [Reverse Proxy](#reverse-proxy)
- [Templates](#templates)
- [`.mj` - Render inline](#mj---render-inline)
- [`.mj compile` / `.mj render` - Precompiled templates](#mj-compile--mj-render---precompiled-templates)
- [Syntax Highlighting](#syntax-highlighting)
- [Streaming Input](#streaming-input)
- [Plugins](#plugins)
- [Embedded Modules](#embedded-modules)
- [Routing](#routing)
- [HTML DSL](#html-dsl)
- [Datastar SDK](#datastar-sdk)
- [Eval Subcommand](#eval-subcommand)
- [Building and Releases](#building-and-releases)
- [Available Build Targets](#available-build-targets)
- [Examples](#examples)
- [GitHub Releases](#github-releases)
- [History](#history)
## Install
### [eget](https://github.com/zyedidia/eget)
```bash
eget cablehead/http-nu
```
### Homebrew (macOS)
```bash
brew install cablehead/tap/http-nu
```
### cargo
```bash
cargo install http-nu --locked
```
### NixOS
http-nu is available in [nixpkgs](https://github.com/NixOS/nixpkgs). For
packaging and maintenance documentation, see
[NIXOS_PACKAGING_GUIDE.md](NIXOS_PACKAGING_GUIDE.md).
## Reference
### GET: Hello world
```bash
Hello world
```
### UNIX domain sockets
```bash
Hello world
```
### Reading closures from stdin
You can also pass `-` as the closure argument to read the closure from stdin:
```bash
Hello from stdin
```
This is especially useful for more complex closures stored in files:
```bash
Check out the [`examples/basic.nu`](examples/basic.nu) file in the repository
for a complete example that implements a mini web server with multiple routes,
form handling, and streaming responses.
#### Dynamic reloads
When reading from stdin, you can send multiple null-terminated scripts to
hot-reload the handler without restarting the server. This example starts with
"v1", then after 5 seconds switches to "v2":
```bash
JSON status is emitted to stdout: `"start"` on first load, `"reload"` on
updates, `"error"` on parse failures.
Watch a file and reload on changes:
```nushell
} | to text | http-nu /run/sock -
```
### POST: echo
```bash
Hai
```
### Request metadata
The Request metadata is passed as an argument to the closure.
```bash
$ http get 'http://localhost:3001/segment?foo=bar&abc=123'
─────────────┬───────────────────────────────
proto │ HTTP/1.1
method │ GET
uri │ /segment?foo=bar&abc=123
path │ /segment
remote_ip │ 127.0.0.1
remote_port │ 52007
trusted_ip │ 127.0.0.1
│ ────────────┬────────────────
headers │ host │ localhost:3001
│ user-agent │ curl/8.7.1
│ accept │ */*
│ ────────────┴────────────────
│ ─────┬─────
query │ abc │ 123
│ foo │ bar
│ ─────┴─────
─────────────┴───────────────────────────────
hello: /yello
```
### Response metadata
You can set the Response metadata using the `.response` custom command.
```nushell
.response {
status: <number> # Optional, HTTP status code (default: 200)
headers: { # Optional, HTTP headers
<key>: <value> # Single value: "text/plain"
<key>: [<value>, <value>] # Multiple values: ["cookie1=a", "cookie2=b"]
}
}
```
Header values can be strings or lists of strings. Multiple values (e.g.,
Set-Cookie) are sent as separate HTTP headers per RFC 6265.
```
HTTP/1.1 404 Not Found
transfer-encoding: chunked
date: Fri, 31 Jan 2025 08:20:28 GMT
sorry, eh
```
Multi-value headers:
```nushell
.response {
headers: {
"Set-Cookie": ["session=abc; Path=/", "token=xyz; Secure"]
}
}
```
### Content-Type Inference
Content-type is determined in the following order of precedence:
1. Headers set via `.response` command:
```nushell
.response {
headers: {
"Content-Type": "text/plain"
}
}
```
2. Pipeline metadata content-type (e.g., from `to yaml`)
3. For Record values with no content-type, defaults to `application/json`
4. Otherwise defaults to `text/html; charset=utf-8`
Examples:
```nushell
# 1. Explicit header takes precedence
# 2. Pipeline metadata
# 3. Record auto-converts to JSON
# 4. Default
### TLS Support
Enable TLS by providing a PEM file containing both certificate and private key:
```bash
Secure Hello
```
Generate a self-signed certificate for testing:
```bash
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
$ cat cert.pem key.pem > combined.pem
```
HTTP/2 is automatically enabled for TLS connections:
```bash
```
### Logging
Control log output with `--log-format`:
- `human` (default): Live-updating terminal output with startup banner,
per-request progress lines showing timestamp, IP, method, path, status,
timing, and bytes
- `jsonl`: Structured JSON lines with `scru128` stamps for log aggregation
Each request emits 3 phases: **request** (received), **response** (headers
sent), **complete** (body finished).
**Human format**
<img width="1835" alt="human format logging output" src="https://github.com/user-attachments/assets/af4f3022-f362-4c93-82c0-5d18ffb3d9ac" />
**JSONL format**
Events share a `request_id` for correlation:
```bash
{"stamp":"...","message":"request","request_id":"...","method":"GET","path":"/","request":{...}}
{"stamp":"...","message":"response","request_id":"...","status":200,"headers":{...},"latency_ms":1}
{"stamp":"...","message":"complete","request_id":"...","bytes":5,"duration_ms":2}
```
Lifecycle events: `started`, `reloaded`, `stopping`, `stopped`, `stop_timed_out`
### Trusted Proxies
When behind a reverse proxy, use `--trust-proxy` to extract client IP from
`X-Forwarded-For`. Accepts CIDR notation, repeatable:
```bash
The `trusted_ip` field is resolved by parsing `X-Forwarded-For` right-to-left,
stopping at the first IP not in a trusted range. Falls back to `remote_ip` when:
- No `--trust-proxy` flags provided
- Remote IP is not in trusted ranges
- No `X-Forwarded-For` header present
### Serving Static Files
You can serve static files from a directory using the `.static` command. This
command takes two arguments: the root directory path and the request path.
When you call `.static`, it sets the response to serve the specified file, and
any subsequent output in the closure will be ignored. The content type is
automatically inferred based on the file extension (e.g., `text/css` for `.css`
files).
Here's an example:
```bash
For single page applications you can provide a fallback file:
```bash
### Streaming responses
Values returned by streaming pipelines (like `generate`) are sent to the client
immediately as HTTP chunks. This allows real-time data transmission without
waiting for the entire response to be ready.
```bash
generate {|_|
sleep 1sec
{out: (date now | to text | $in + "\n") next: true }
} true
}'
$ curl -s localhost:3001
Fri, 31 Jan 2025 03:47:59 -0500 (now)
Fri, 31 Jan 2025 03:48:00 -0500 (now)
Fri, 31 Jan 2025 03:48:01 -0500 (now)
Fri, 31 Jan 2025 03:48:02 -0500 (now)
Fri, 31 Jan 2025 03:48:03 -0500 (now)
...
```
### [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
Use the `to sse` command to format records for the `text/event-stream` protocol.
Each input record may contain the optional fields `data`, `id`, `event`, and
`retry` which will be emitted in the resulting stream.
#### `to sse`
Converts `{data? id? event? retry?}` records into SSE format. Non-string `data`
values are serialized to JSON.
Auto-sets response headers: `content-type: text/event-stream`,
`cache-control: no-cache`, `connection: keep-alive`.
| record | string |
Examples
```bash
> {data: 'hello'} | to sse
data: hello
> {id: 1 event: greet data: 'hi'} | to sse
id: 1
event: greet
data: hi
> {data: "foo\nbar"} | to sse
data: foo
data: bar
> {data: [1 2 3]} | to sse
data: [1,2,3]
```
```bash
$ http-nu :3001 '{|req|
.response {headers: {"content-type": "text/event-stream"}}
tail -F source.json | lines | from json | to sse
}'
# simulate generating events in a seperate process
$ loop {
{date: (date now)} | to json -r | $in + "\n" | save -a source.json
sleep 1sec
}
$ curl -si localhost:3001/
HTTP/1.1 200 OK
content-type: text/event-stream
transfer-encoding: chunked
date: Fri, 31 Jan 2025 09:01:20 GMT
data: {"date":"2025-01-31 04:01:23.371514 -05:00"}
data: {"date":"2025-01-31 04:01:24.376864 -05:00"}
data: {"date":"2025-01-31 04:01:25.382756 -05:00"}
data: {"date":"2025-01-31 04:01:26.385418 -05:00"}
data: {"date":"2025-01-31 04:01:27.387723 -05:00"}
data: {"date":"2025-01-31 04:01:28.390407 -05:00"}
...
```
### Reverse Proxy
You can proxy HTTP requests to backend servers using the `.reverse-proxy`
command. This command takes a target URL and an optional configuration record.
When you call `.reverse-proxy`, it forwards the incoming request to the
specified backend server and returns the response. Any subsequent output in the
closure will be ignored.
**What gets forwarded:**
- HTTP method (GET, POST, PUT, etc.)
- Request path and query parameters
- All request headers (with Host header handling based on `preserve_host`)
- Request body (whatever you pipe into the command)
**Host header behavior:**
- By default: Preserves the original client's Host header
(`preserve_host: true`)
- With `preserve_host: false`: Sets Host header to match the target backend
hostname
#### Basic Usage
```bash
# Simple proxy to backend server
#### Configuration Options
The optional second parameter allows you to customize the proxy behavior:
```nushell
.reverse-proxy <target_url> {
headers?: {<key>: <value>} # Additional headers to add
preserve_host?: bool # Keep original Host header (default: true)
strip_prefix?: string # Remove path prefix before forwarding
query?: {<key>: <value>} # Replace query parameters (Nu record)
}
```
#### Examples
**Add custom headers:**
```bash
headers: {
"X-API-Key": "secret123"
"X-Forwarded-Proto": "https"
}
}
}'
```
**API gateway with path stripping:**
```bash
strip_prefix: "/api/v1"
}
}'
# Request to /api/v1/users becomes /users at the backend
```
**Forward original request body:**
```bash
```
**Override request body:**
```bash
```
**Modify query parameters:**
```bash
query: ($req.query | upsert "context-id" "smidgeons" | reject "debug")
}
}'
# Force context-id=smidgeons, remove debug param, preserve others
```
### Templates
Render [minijinja](https://github.com/mitsuhiko/minijinja) (Jinja2-compatible)
templates. Pipe a record as context.
#### `.mj` - Render inline
```bash
Hello world!
```
From a file:
```bash
#### `.mj compile` / `.mj render` - Precompiled templates
Compile once, render many. Syntax errors caught at compile time.
```nushell
let tpl = (.mj compile --inline "{{ name }} is {{ age }}")
# Or from file
let tpl = (.mj compile "templates/user.html")
# Render with data
Useful for repeated rendering:
```nushell
let tpl = (.mj compile --inline "{% for i in items %}{{ i }}{% endfor %}")
[{items: [1,2,3]}, {items: [4,5,6]}] | each { .mj render $tpl }
```
Compile once at handler load, render per-request:
```nushell
let page = .mj compile "templates/page.html"
With HTML DSL (accepts `{__html}` records directly):
```nushell
use http-nu/html *
let tpl = .mj compile --inline (UL (_for {item: items} (LI (_var "item"))))
```
### Syntax Highlighting
Highlight code to HTML with CSS classes.
```bash
$ .highlight lang # list languages
$ .highlight theme # list themes
$ .highlight theme Dracula # get CSS
```
### Streaming Input
In Nushell, input only streams when received implicitly. Referencing `$in`
collects the entire input into memory.
```nushell
# Streams: command receives input implicitly
# Buffers: $in collects before piping
For routing, `dispatch` must be first in the closure to receive the body. In
handlers, put body-consuming commands first:
```nushell
(route {method: "POST"} {|req ctx|
from json # receives body implicitly
})
]
}
```
### Plugins
Load Nushell plugins to extend available commands.
```bash
```bash
context passed to handler) or null (no match). If no routes match, returns
`501 Not Implemented`.
#### HTML DSL
Build HTML with Nushell. Lisp-style nesting with uppercase tags.
```nushell
use http-nu/html *
(HEAD (TITLE "Demo"))
(BODY
(H1 "Hello")
(P {class: "intro"} "Built with Nushell")
(UL { 1..3 | each {|n| LI $"Item ($n)" } })
)
)
}
```
`HTML` automatically prepends
[`<!DOCTYPE html>`](https://html.spec.whatwg.org/multipage/syntax.html#the-doctype).
All HTML5 elements available as uppercase commands (`DIV`, `SPAN`, `UL`, etc.).
Attributes via record, children via args or closure. Lists from `each` are
automatically joined. Plain strings are auto-escaped for XSS protection;
`{__html: "<b>trusted</b>"}` bypasses escaping for pre-sanitized content.
`style` accepts a record; values can be lists for comma-separated CSS (e.g.
`font-family`): `{style: {font-family: [Arial sans-serif] padding: 10px}}`
`class` accepts a list: `{class: [card active]}`
[Boolean attributes](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML):
`true` renders the attribute, `false` omits it:
```nushell
INPUT {type: "checkbox" checked: true disabled: false}
# <input type="checkbox" checked>
```
**Jinja2 Template DSL**
For hot paths, `_var`, `_for`, and `_if` generate Jinja2 syntax that can be
compiled once and rendered repeatedly (~200x faster than the runtime DSL):
```nushell
_var "user.name" # {{ user.name }}
_for {item: items} (LI (_var "item")) # {% for item in items %}...{% endfor %}
_if "show" (DIV "content") # {% if show %}...{% endif %}
```
```nushell
let tpl = .mj compile --inline (UL (_for {item: items} (LI (_var "item"))))
```
#### Datastar SDK
Generate [Datastar](https://data-star.dev) SSE events for hypermedia
interactions. Follows the
[SDK ADR](https://github.com/starfederation/datastar/blob/develop/sdk/ADR.md).
Commands return records that pipe to `to sse` for streaming output.
```nushell
use http-nu/datastar *
use http-nu/html *
let signals = from datastar-request $req
[
# Update DOM
(DIV {id: "notifications" class: "alert"} "Profile updated!"
| to dstar-patch-element)
# Or target by selector
(DIV {class: "alert"} "Profile updated!"
| to dstar-patch-element --selector "#notifications")
# Update signals
({count: ($signals.count + 1)} | to dstar-patch-signal)
# Execute script
("console.log('updated')" | to dstar-execute-script)
]
| to sse
}
```
**Commands:**
```nushell
to dstar-patch-element [
--selector: string # CSS selector (omit if element has ID)
--mode: string # outer, inner, replace, prepend, append, before, after, remove (default: outer)
--namespace: string # Content namespace: html (default) or svg
--use_view_transition # Enable CSS View Transitions API
--id: string # SSE event ID for replay
--retry: int # Reconnection delay in ms
]: string -> record
to dstar-patch-signal [
--only_if_missing # Only set signals not present on client
--id: string
--retry: int
]: record -> record
to dstar-execute-script [
--auto_remove: bool # Remove <script> after execution (default: true)
--attributes: record # HTML attributes for <script> tag
--id: string
--retry: int
]: string -> record
## Eval Subcommand
Test http-nu commands without running a server.
```bash
# From command line
$ http-nu eval -c '1 + 2'
3
# From file
$ http-nu eval script.nu
# From stdin
# Test .mj commands
```
## Building and Releases
This project uses [Dagger](https://dagger.io) for cross-platform containerized
builds that run identically locally and in CI. This means you can test builds on
your machine before pushing tags to trigger releases.
### Available Build Targets
- **Windows** (`windows-build`)
- **macOS ARM64** (`darwin-build`)
- **Linux ARM64** (`linux-arm-64-build`)
- **Linux AMD64** (`linux-amd-64-build`)
### Examples
Build a Windows binary locally:
```bash
dagger call windows-build --src upload --src "." export --path ./dist/
```
Get a throwaway terminal inside the Windows builder for debugging:
```bash
dagger call windows-env --src upload --src "." terminal
```
**Note:** Requires Docker and the [Dagger CLI](https://docs.dagger.io/install).
The `upload` function filters files to avoid uploading everything in your local
directory.
### GitHub Releases
The GitHub workflow automatically builds all platforms and creates releases when
you push a version tag (e.g., `v1.0.0`). Development tags containing `-dev.` are
marked as prereleases.
## History
If you prefer POSIX to [Nushell](https://www.nushell.sh), this project has a
cousin called [http-sh](https://github.com/cablehead/http-sh).