http-nu

The surprisingly performant, Nushell-scriptable HTTP server that fits in your back pocket.
Install
eget
cargo
NixOS
http-nu is available in nixpkgs. For packaging and maintenance documentation, see NIXOS_PACKAGING_GUIDE.md.
Overview
GET: Hello world
UNIX domain sockets
Reading closures from stdin
You can also pass - as the closure argument to read the closure from stdin:
|
This is especially useful for more complex closures stored in files:
|
Check out the 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":
; ; ) |
JSON status is emitted to stdout: "start" on first load, "reload" on
updates, "error" on parse failures.
POST: echo
Request metadata
The Request metadata is passed as an argument to the closure.
&abc=123
Response metadata
You can set the Response metadata using the .response custom command.
.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-nu :3001 '{|req| .response {status: 404}; "sorry, eh"}'
$ curl -si localhost:3001
HTTP/1.1 404 Not Found
transfer-encoding: chunked
date: Fri, 31 Jan 2025 08:20:28 GMT
sorry, eh
Multi-value headers:
.response {
headers: {
"Set-Cookie": ["session=abc; Path=/", "token=xyz; Secure"]
}
}
Content-Type Inference
Content-type is determined in the following order of precedence:
-
Headers set via
.responsecommand:.response { headers: { "Content-Type": "text/plain" } } -
Pipeline metadata content-type (e.g., from
to yaml) -
For Record values with no content-type, defaults to
application/json -
Otherwise defaults to
text/html; charset=utf-8
Examples:
# 1. Explicit header takes precedence
{|req| .response {headers: {"Content-Type": "text/plain"}}; {foo: "bar"} } # Returns as text/plain
# 2. Pipeline metadata
{|req| ls | to yaml } # Returns as application/x-yaml
# 3. Record auto-converts to JSON
{|req| {foo: "bar"} } # Returns as application/json
# 4. Default
{|req| "Hello" } # Returns as text/html; charset=utf-8
TLS Support
Enable TLS by providing a PEM file containing both certificate and private key:
Generate a self-signed certificate for testing:
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:
For single page applications you can provide a fallback file:
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.
)
)
)
)
)
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, and event
which will be emitted in the resulting stream.
to sse
Converts {data? id? event?} records into SSE strings. String values are used
as-is while other values are serialized to compact JSON. Each event ends with an
empty line.
| input | output |
|---|---|
| record | string |
Examples
> |
> |
> |
> |
# simulate generating events in a seperate process
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
# Simple proxy to backend server
Configuration Options
The optional second parameter allows you to customize the proxy behavior:
.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:
API gateway with path stripping:
# Request to /api/v1/users becomes /users at the backend
Forward original request body:
# If .reverse-proxy is first in closure, original body is forwarded (implicit $in)
Override request body:
# Whatever you pipe into .reverse-proxy becomes the request body
Modify query parameters:
# Force context-id=smidgeons, remove debug param, preserve others
Templates
Render minijinja (Jinja2-compatible)
templates with the .mj command. Pipe a record as context.
From a file:
Building and Releases
This project uses Dagger 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:
Get a throwaway terminal inside the Windows builder for debugging:
Note: Requires Docker and the Dagger CLI.
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, this project has a cousin called http-sh.