A powerful terminal-based HTTP client with a versionable, template-driven configuration system.
Nightmare is a modern .NET terminal UI (TUI) application for composing, organizing, and executing HTTP requests from a single JSON configuration file. Featuring a built-in expression language with 23+ functions, intelligent response caching, and hot-reload support, Nightmare streamlines API testing and development workflows without requiring code or external tools.
Key Features:
- π₯ Hot-Reload Configuration - Changes to your config are reflected instantly
- π― Template Expression Language - Dynamic values, variable substitution, and function calls
- π Request/Response Chaining - Reference previous responses in subsequent requests
- π¦ Multi-Environment Profiles - Switch between dev/staging/prod with one keypress
- πΎ Smart Response Caching - Configurable TTL to avoid redundant API calls
- ποΈ Organized Request Trees - Nested groups for logical request organization
- π€ Multi-Format Bodies - Support for JSON, form-data, multipart, and raw content
- π Interactive Prompts - Request user input or file paths at runtime
- π Native AOT Compilation - Fast startup, low memory footprint
- Installation
- Quick Start
- Configuration Format
- Template Expression Language
- Request & Response Chaining
- Advanced Usage
- Keyboard Shortcuts
- Examples
- Contributing
- License
Download the latest release for your platform from the Releases page:
- Windows:
nightmare-win-x64.zip - Linux:
nightmare-linux-x64.tar.gz - macOS (ARM):
nightmare-osx-arm64.tar.gz - macOS (Intel):
nightmare-osx-x64.tar.gz
Extract and run the nightmare executable.
Prerequisites:
Build steps:
git clone https://github.com/LinkNexus/Nightmare.git
cd Nightmare/Nightmare
dotnet publish -c Release -r <your-runtime-id>The compiled binary will be in bin/Release/net10.0/<runtime-id>/publish/.
Available runtime identifiers: win-x64, linux-x64, osx-x64, osx-arm64.
- Run
nightmare newto create a new config file in the current directory. - The following options are available:
--file <path>or-f <path>: specify a custom file name (defaults tonightmare.jsonin the current directory).
- Run
nightmareto launch the app. - The following options are available:
--config <path>or-c <path>: specify a custom config file (defaults tonightmare.jsonin the current directory).--depth <n>or-d <n>: search upward for a config file up tondirectories (defaults to 5).
- Outputs a TUI with the selected profile (defaults to the first profile with default prop as true), requests tree, a request section that shows the details of the selected request and a response section that shows the details of the response of this request.
- Press
P(when the focus is not in the request tree) to open the profile dialog (where you can switch profiles) - There is hot reload for changes to the config file.
{
"name": "My API Requests",
"profiles": {
"dev": {
"default": true,
"data": {
"base_url": "https://httpbin.org"
}
}
},
"requests": {
"hello": {
"url": "hello.com"
}
}
}- The request tree will be formed from the entries under
requests. - Each entry can be a request definition or a nested group of requests.
- In order to make an entry a group of requests, add a
requestsproperty to the entry as following:{ "requests": { "request1": { "url": "hello.com" } "request2": { "requests": { "subrequest1": { "url": "hello.com" } } } } }
- The
urlproperty is required for all requests. - The
methodproperty defaults toGET. - Each request can have a
timeoutproperty (in milliseconds). - The query object specifies the query params to be added to the URL. Note, in the TUI, the existing query params in the url are added with these from the query object. The values of the query object are serialized to strings in case of non-string values.
- The headers object specifies the headers to be sent with the request. As with query, the values are serialized to strings.
- The cookies object specifies the cookies to be sent with the request. Values are serialized to strings.
- The body property specifies the body of the request.
- The value of the body can be a simple string, or a string with template expressions (explained later in these docs) that can evaluated to any value (non-string values will be serialized to strings). In this case, the type is considered
raw. Can be used to send strings or files with the{{ files() }}function. - Or an object with following props
type: Can be of valueraw,text,json,form,multipart. Defaults toraw.value: The value of the body. The value depends on the type.
- The value of the body can be a simple string, or a string with template expressions (explained later in these docs) that can evaluated to any value (non-string values will be serialized to strings). In this case, the type is considered
raw: The body must be a string with or without template expressions that can evaluated to any value (non-string values will be serialized to strings).textandjson: The body can be any value, from string with(out) template expressions to objects/arrays etc... They are serialized to strings. The content type is set accordingly.form: The body must either be an object or a string with template expressions that can evaluate to an object. The values are serialized to strings, but for arrays types, the values are appended to each key. Sent as form-urlencoded data.multipart: As inform, the value must evaluate to an object-like value. The values are serialized to strings, but for arrays types, the values are appended to each key. Sent as multipart/form-data. Files can be added by using thefile(...)function.
{
"name": "{{ upper('Nightmare') }} Requests Collection",
"profiles": {
"dev": {
"default": true,
"data": {
"base_url": "https://httpbin.org",
"user": {
"username": "admin",
"password": "{{ prompt('Enter a password for the dev profile:') }}"
}
}
}
},
"requests": {
"auth": {
"requests": {
"login": {
"url": "{{ base_url }}/post",
"method": "POST",
"body": { "type": "json", "value": "{{ user }}" }
}
}
}
}
}- Expressions are wrapped in double curly braces
{{ expr }}inside strings. - The expression is evaluated against the current context (profile data which serves as variables which can be used in expressions).
- The result of the expression is not necessarily evaluated to a string. For example: the {{ user }} in the example above evaluates to an object. This only works when the expression is the only thing in the json string. Things like
{{ user }} Hellowill call the ToString C# method on the object creating weird results like System.Dictionnary2[System.String,System.Object] Hello. - The expression can be a simple identifier (e.g.,
base_url) or a member/index access expression (e.g.,user.username) or a string litteral/number (e.g.,'hello'/1), or a function call (e.g.,concat(...)) or booleans (true/false) ornull. - Notice the single quotes around the string literal (Double quotes, even escaped ones, are not supported)
- Operations are permitted (like addition (+), subtraction (-), division (/), multiplication (*), modulo (%)), even exponential expression with
2e5are supported. - Even ternary expressions are supported (
condition ? trueVal : falseVal). - The expression can be wrapped in parentheses to force precedence.
- The expression can be a member access expression on a function call (e.g.,
jsonDecode(jsonString).prop). - The expression can be a function call with a variable number of arguments (e.g.,
concat('hello', 'hi')) - You can
add2 string literals with the+operator. This will result in a string concatenation. - Here is the list of the operators and their precedence: (precedence low β high): ternary
?:;||;&&;== !=;< <= > >=;+ -;* / %; unary! -; postfix. [] (). - Template expressions can be embedded in strings like normal, for example:
{{ upper('h') + 'ello' }} World! - As shown, most functions can be used in the
profilessection of the config. - Each time the config file is modifiied and the app is reloaded, the selected profile data is reevaluated (This may be annoying when using the
promptorfilePromptfunctions). - Property names cannot contain expressions. They will not be evaluated.
All functions throw descriptive errors when argument types/counts are invalid. Signatures use [] for optional parameters and ... for variadic args.
| Name | Signature | Description |
|---|---|---|
upper |
upper(input: string) | Uppercase. |
lower |
lower(input: string) | Lowercase. |
concat |
concat(values: string...) | Concatenate all values. |
date |
date(time?: string | number = "now", format?: string = "yyyy-MM-dd HH:mm:ss") | Format unix seconds, now, or relative strings like "5 days", "-2 hours". The time identifier must always be in plural (not 1 day but 1 days) |
timestamp |
timestamp() | Current unix timestamp (seconds). |
env |
env(name: string, defaultValue?: string | null = null) | Environment variable fallback. |
hash |
hash(input: string | number) | SHA-256 hex string. |
ifElse |
ifElse(condition: bool, ifVal: any, elseVal: any) | Ternary helper. |
len |
len(value: string | array) | Length/count. |
max |
max(numbers: number...) | Maximum; at least one value required. |
min |
min(numbers: number...) | Minimum; at least one value required. |
readFile |
readFile(path: string) | File contents as text. |
file |
file(path: string) | FileReference for multipart/raw bodies (adds path; name inferred on send). |
filePrompt |
filePrompt() | Opens a file picker; returns selected path. |
prompt |
prompt(message?: string = "Enter the value: ") | Text prompt dialog. |
uuid |
uuid() | New GUID string. |
urlEncode |
urlEncode(input: string) | URL-encode. |
urlDecode |
urlDecode(input: string) | URL-decode. |
base64Encode |
base64Encode(input: string) | UTF-8 β Base64. |
base64Decode |
base64Decode(input: string) | Base64 β UTF-8. |
jsonEncode |
jsonEncode(value: string | number | bool | null | array | object) | Pretty-print JSON. |
jsonDecode |
jsonDecode(jsonString: string, parseTemplates?: bool = false) | Parse JSON to objects/arrays; when parseTemplates is true, expressions inside strings are evaluated against the current context. |
req |
req(requestId: string) | Fetch a request definition by id (section.subsection.leaf). |
res |
res(requestId: string, trigger?: string = "10 minutes") | Return cached response or execute the request; stale cache re-executes. |
- Keep reusable values in
profiles.data; they are exposed as identifiers everywhere expressions are allowed. - Use
req(...)to avoid duplicating request fragments andres(...)to chain workflows (e.g., login β reuse cookies/body in later calls). - For file uploads, wrap paths with
file("/path/to/file"); for interactive picking, callfilePrompt()inside the expression.