http-nu 
From shell to web: http-nu serves your Nushell
closure over HTTP.
Install
Overview
GET: Hello world
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.
You can listen to UNIX domain sockets as well
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:
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>
}
}
$ 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
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
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
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.