Skip to content
HecatoncheirPublic

About

Terminal UI for the .http, .hurl and .socket requests in your project — live WebSocket, MQTT and STOMP streams, environment profiles, GraphQL, syntax-highlighted responses, and Vim-style navigation.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

lazyrest app icon

lazyrest

REST at your fingertips.

CI License

lazyrest is a terminal UI for discovering and running requests from .http, .hurl and .socket files, including the connections that stay open.

Preview

lazyrest screenshot

lazyrest demo

Watch the full MOV preview

Features

  • Immediate TUI startup with background environment loading and recursive .http / .hurl / .socket discovery.
  • Pure Go, with no CGO and no external parser: go install and cross-compilation need nothing but the Go toolchain.
  • Requests chained through what an earlier one answered, so a token is captured rather than copied by hand.
  • Automatic private .env loading, public/private environment profiles, and recursive {{variable}} substitution shared with Hurl.
  • Cookies carried from one request to the next, with control over redirects and certificate checks.
  • GraphQL requests encoded the way servers expect, with a variables block and errors surfaced from 200 responses.
  • WebSocket and MQTT 5 sessions run from .http files, addressed by a ws://, wss://, mqtt:// or mqtts:// URL, and raw TCP from .socket files. A live pane shows every frame with its direction, size and time; an MQTT message shows the topic it arrived on.
  • STOMP brokers such as RabbitMQ are reachable over both transports: a frame ends on the null byte the protocol requires, written \0.
  • A stream pane follows or pauses, clears without hanging up, and sends frames from a composer that remembers what was sent before.
  • .hurl files listed one entry at a time, each run with the entries it depends on.
  • Syntax highlighting for JSON, XML, and GraphQL across the panes, and HTTP methods coloured by what they do.
  • Response headers, protocol metadata, Pretty/Raw bodies, and clipboard/file export.
  • One-key request replay, environment switching, and executable cURL export from Producer.
  • Cancellable execution with animated progress bars, a timeout, and bounded response bodies.
  • File/request/response search, dedicated diagnostics and history windows, and a metadata-only persistent history of the last 50 runs per project.
  • Mouse and Vim-style keyboard navigation.

Requirements

  • Go 1.25.13 or newer when building from source.
  • The hurl executable for .hurl files. It is not required for ordinary .http requests.

Installation

Prebuilt archives for Linux, macOS, and Windows are attached to every release. Download the one matching your platform, verify it against SHA256SUMS, and put lazyrest on your $PATH:

tar -xzf lazyrest-linux-amd64.tar.gz && sudo install lazyrest /usr/local/bin/

macOS builds are not notarized, so the first launch needs xattr -d com.apple.quarantine lazyrest.

Or install from source, which needs nothing but the Go toolchain:

go install github.com/Hecatoncheir/lazyrest@latest

Or build the repository locally:

go build -o lazyrest .

The build is pure Go, so cross-compiling asks for no C toolchain:

env GOOS=windows GOARCH=amd64 go build

Neovim and LazyVim

Install lazyrest first and make sure the executable is available in Neovim's $PATH:

go install github.com/Hecatoncheir/lazyrest@latest

Neovim

The following configuration opens lazyrest in a terminal tab, using the nearest Git repository or Go module as its root. Add it to init.lua:

local function open_lazyrest()
  local root = vim.fs.root(0, { ".git", "go.mod" }) or vim.fn.getcwd()

  vim.cmd("tabnew")
  local job = vim.fn.jobstart({ "lazyrest", root }, {
    cwd = root,
    term = true,
  })
  if job <= 0 then
    vim.notify("Unable to start lazyrest; check $PATH", vim.log.levels.ERROR)
    vim.cmd("tabclose")
    return
  end
  vim.cmd("startinsert")
end

vim.api.nvim_create_user_command("LazyRest", open_lazyrest, {})
vim.keymap.set("n", "<leader>Rl", open_lazyrest, { desc = "LazyRest" })

Run :LazyRest or press <leader>Rl. Use <C-\\><C-n> to leave terminal mode and :tabclose to close the tab after lazyrest exits.

LazyVim

LazyVim includes snacks.nvim, whose terminal can toggle the same process in a floating window. Create ~/.config/nvim/lua/plugins/lazyrest.lua:

return {
  {
    "folke/snacks.nvim",
    keys = {
      {
        "<leader>Rl",
        function()
          local root = vim.fs.root(0, { ".git", "go.mod" }) or vim.fn.getcwd()
          Snacks.terminal.toggle({ "lazyrest", root }, {
            cwd = root,
            start_insert = true,
          })
        end,
        desc = "LazyRest",
      },
    },
  },
}

The mapping follows LazyVim's REST key namespace. While lazyrest is in Terminal mode, Ctrl+h/j/k/l are sent to the TUI. Use <C-\\><C-n> first when you want those keys to control Neovim windows instead.

To select an environment profile, place its flags before the directory in either example:

{ "lazyrest", "-env", "development", root }

Usage

lazyrest [flags] [directory]

When the directory is omitted, the current working directory is used.

-timeout duration         request and Hurl timeout (default 30s)
-max-response-bytes int   maximum response bytes kept in memory (default 10485760)
-max-redirects int        maximum redirects a request follows (default 10)
-follow-redirects         follow redirects instead of returning them (default true)
-cookies                  carry cookies from one request to the next (default true)
-insecure                 accept any server certificate
-hurl string              Hurl executable name or path (default "hurl")
-env string               environment profile name
-dotenv-file string       dotenv file loaded as a private base environment (default ".env")
-env-file string          public environment file (default "http-client.env.json")
-private-env-file string  private environment file (default "http-client.private.env.json")
-version                  print the version and exit

The flags that read and write configuration files are described under Configuration.

Example .http file:

@host = "api.example.com"
@token = "development-token"

# @name List users
GET https://{{host}}/users
Authorization: Bearer {{token}}
Accept: application/json

A request body can be read from a file next to the .http file. Variables inside the file are substituted as usual:

POST https://{{host}}/users
Content-Type: application/json

< ./payload.json

Sensitive headers such as Authorization, cookies, API keys, tokens, and secrets are redacted from the response pane.

Cookies a server sets are carried into the requests that follow, so a login request holds a session for the rest of the run. The jar lives in memory only and is dropped when lazyrest exits; -cookies=false turns it off. Redirects are followed up to -max-redirects, and -follow-redirects=false returns the redirect itself so that its Location can be read. -insecure accepts any certificate, for a host serving a self-signed one.

GraphQL

A request is treated as GraphQL when its body is a GraphQL document or when it carries X-REQUEST-TYPE: GraphQL. Write the query, then an optional JSON object of variables after a blank line:

POST https://{{host}}/graphql
X-REQUEST-TYPE: GraphQL

query GetUser($id: ID!) {
  user(id: $id) { name }
}

{
  "id": "{{userId}}"
}

lazyrest sends this as application/json with a {"query": …, "variables": …} body, which is what the GraphQL over HTTP specification requires and what servers accept. When the document names exactly one operation, its name is sent as operationName.

GraphQL answers with 200 even when the operation failed, so an errors array in the response is listed separately and marks the run as failed. To send the raw query instead, declare Content-Type: application/graphql yourself.

Chaining requests

A request can use what an earlier one answered. Name the first request, run it, then refer to its response:

# @name login
POST https://api.example.com/auth
Content-Type: application/json

{"user": "me", "password": "secret"}

###

GET https://api.example.com/profile
Authorization: Bearer {{login.response.body.$.token}}

{{name.response.body.$.path}} reads a value out of a JSON body, with member names and array indices under a $ root, such as {{login.response.body.$.data.items[0].id}}. A string is inserted as itself and anything else as its JSON form. {{name.response.body}} takes the whole body, and {{name.response.headers.X-Token}} takes a response header.

References are resolved when the request runs, against the last answer of each named request from the same file in the current session. Identical names in different files do not share captured responses. Nothing is stored on disk. A request whose references cannot be resolved is not sent; the pane says which reference was waiting and why.

Environments

A .env file in the project root is loaded automatically, even when no profile is selected:

HOST=api.example.com
TOKEN="local-secret"
BASE_URL=https://{{HOST}}/v1

Use these values as {{HOST}}, {{TOKEN}}, or {{BASE_URL}} in .http files. All .env values are treated as private: they are passed to Hurl through a private file and redacted from requests, responses, errors, and history output. Use -dotenv-file .env.local to select a different project-root filename.

Select a profile with lazyrest -env development .. Public profile values come from http-client.env.json:

{
  "development": {
    "host": "api.example.com",
    "api": { "version": "v1" },
    "baseUrl": "https://{{host}}/{{api.version}}"
  }
}

Put secrets in http-client.private.env.json, which is ignored by Git:

{
  "development": { "token": "local-secret" }
}

Use profile values as {{baseUrl}} or {{token}} in .http files. .env provides the base layer, a selected public profile overrides it, the selected private profile overrides both, and declarations inside an .http file have the final word. Undefined variables and reference cycles appear as parser diagnostics. Private values are redacted from requests, responses, errors, and history output.

Choose Choose environment from the command palette to switch between the base .env values and profiles found in either JSON file without restarting. lazyrest reloads the selected environment, clears captured responses and cookies from the previous one, and reparses the open request file immediately.

Examples

The example directory contains ready-to-run .http, .hurl and .socket files with named requests, variables, different body formats, assertions, a multi-request Hurl workflow, and streams over WebSocket, MQTT and STOMP:

go run . example

The examples use the public https://httpbin.org test service and require internet access. Running .hurl examples also requires the hurl executable.

A .hurl file is listed one entry at a time. Hurl runs a file in order and an entry may use what an earlier one captured, so selecting an entry runs the file up to it with --to-entry; the last entry therefore runs the whole file. The selected exchange is rendered like an ordinary HTTP response, including its status, headers, protocol, body, and any failed assertions.

Configuration

lazyrest merges configuration in this order, with later layers taking priority:

  1. Built-in defaults.
  2. ~/.config/lazyrest/config.yml.
  3. .lazyrest.yml in the selected project root.
  4. The file passed with --config.

Each action accepts one or more keys. Configured keys replace the defaults for that action; actions omitted from the file keep their default bindings. Reusing a key in separate panels is allowed, while conflicting actions in the same context fail validation with an actionable error.

language: zh
history: metadata

ignore:
  - fixtures
  - generated

theme:
  preset: catppuccin-mocha
  accent: "#89b4fa"

languages:
  en:
    files: "Files"
    suites: "Suites"
    producer: "Producer"
    success: "Success"
    failed: "Failed"
  ru:
    files: "Файлы"
    suites: "Запросы"
    producer: "Результат"
    success: "Успешно"
    failed: "Ошибка"
  es:
    files: "Archivos"
    suites: "Solicitudes"
    producer: "Resultado"
    success: "Correcto"
    failed: "Error"
  zh:
    files: "文件"
    suites: "请求列表"
    producer: "响应"
    success: "成功"
    failed: "失败"

keybindings:
  help: ["?", "f1"]
  diagnostics: ["d"]
  quit: ["q", "ctrl+c"]
  focus_left: ["ctrl+h"]
  focus_down: ["ctrl+j"]
  focus_up: ["ctrl+k"]
  focus_right: ["ctrl+l"]
  open: ["enter", "l"]
  run: ["enter", "r"]
  back: ["esc"]
  search: ["/"]
  search_finish: ["enter", "esc"]
  search_next: ["n"]
  search_previous: ["N"]
  reload: ["r"]
  move_down: ["j"]
  move_up: ["k"]
  half_page_down: ["ctrl+d"]
  half_page_up: ["ctrl+u"]
  page_down: ["ctrl+f"]
  page_up: ["ctrl+b"]
  go_to_top: ["gg"]
  go_to_bottom: ["G"]
  align_top: ["zt"]
  center_view: ["zz"]
  align_bottom: ["zb"]
  toggle_body: ["p"]
  toggle_headers: ["h"]
  toggle_request_details: ["i"]
  rerun_request: ["R"]
  copy_response_body: ["y"]
  copy_response: ["Y"]
  copy_as_curl: ["C"]
  save_response: ["s"]
  save_full_response: ["S"]
  clear_captured_responses: ["c"]
  clear_history: ["c"]
  history_previous: ["["]
  history_next: ["]"]
  stream_follow: ["f"]
  stream_clear: ["c"]
  stream_send: ["s"]
  stream_recall_previous: ["up"]
  stream_recall_next: ["down"]
  command_palette: [":", "ctrl+p"]
  reload_config: ["ctrl+r"]

Producer opens with the response body first. Press h to show response headers and i to show the resolved request; the H+/H− and R+/R− indicators in its title show which details are visible.

ignore names directories the file tree does not descend into, on top of the built-in list: .git, .hg, .svn, .cache, .venv, .tox, node_modules, vendor, target, dist, and build. The lists of the configuration layers add up, so a project can skip more without losing what you chose. Files and directories matched by .gitignore files in the project root or nested directories are also skipped. Git globs, anchored and directory-only patterns, **, and ! negation use normal Git precedence; invalid patterns produce a diagnostic. Symbolic links to directories are followed once each, and a scan stops at 32 directories deep with a warning in the diagnostics window.

The built-in languages are English (en), Russian (ru), Spanish (es), and Simplified Chinese (zh). The languages section is optional and overrides individual built-in strings. Missing strings fall back to the selected built-in language and then to English. A new language can be added by defining it under languages and selecting its code with language.

Syntax highlighting takes its colours from the same palette, so it matches whichever preset or override is active. It covers the body preview in Suites, the request in Suite, and the request and response in Producer. Bodies over 256 KiB are shown without highlighting to keep the panes responsive.

The built-in theme presets are gruvbox (default), catppuccin-mocha, tokyo-night, dracula, nord, and monokai. Every theme color remains an optional hexadecimal RGB override applied on top of the selected preset. Choose Choose theme from the command palette to switch immediately and save the selected preset for future sessions. The highest-priority configuration layer that already defines theme.preset is updated; otherwise the choice is written to the user configuration without creating a project file. Press Ctrl+r or choose Reload configuration to apply language, translations, keybindings, theme, and history-mode changes from configuration without restarting lazyrest. Invalid configuration leaves the current settings active and displays an error in the footer.

Supported named keys are enter, esc, backspace, tab, arrow keys, home, end, pgup, pgdn, f1 through f12, and ctrl+a through ctrl+z. Single non-whitespace printable characters are case-sensitive. Viewport actions also accept printable key sequences such as gg, zt, zz, and zb.

Configuration CLI commands:

lazyrest --generate-config
lazyrest --print-config /path/to/project
lazyrest --validate-config /path/to/project
lazyrest --config ./team.yml /path/to/project
lazyrest --config ./custom.yml --generate-config

--generate-config creates a complete configuration with permissions 0600 and refuses to overwrite an existing file. --print-config prints the resolved layered configuration. --validate-config checks YAML, unknown keys, colors, languages, key names, and contextual key conflicts without starting the TUI. A key the configuration does not define is an error rather than something quietly dropped, so a typo such as keybinding for keybindings is reported with its line.

Persistent history

The latest 50 request results are stored separately for every project under ~/.config/lazyrest/history/<project-id>.json and restored only when that same canonical project root is opened again. The previous shared history.json is left untouched rather than importing its mixed entries into one project.

history: metadata is the default. It persists only the request name and method plus response status, timing, size, and protocol. URLs, headers, bodies, error details, GraphQL data, and runnable request state remain in memory for the current session and are omitted from disk. A restored metadata entry explains that its details were not persisted and cannot be repeated or exported.

Set history: full in the configuration only when restored request and response details are useful enough to justify storing them. Bodies are then limited to 64 KiB per entry. Sensitive headers such as Authorization, cookies, and API keys are redacted, as are known secrets, credentials found in common JSON token or session fields, and values captured through sensitive response references. Switching back to metadata mode rewrites previously stored detailed entries the next time the project is opened or configuration is reloaded.

Writing happens in the background so a large response does not stall the interface. The directory uses permissions 0700, history files use 0600, and updates are atomic. A corrupted history file or failed background write is kept as a deduplicated entry in Diagnostics; lazyrest continues running with the history that remains available in memory.

Choose History from the command palette to see the project's entries newest first, with request name, source file, timestamp, status, and duration but no body or header values. Press Enter or l to open an entry in Producer. Press c twice to clear both the in-memory list and the project's persisted entries; Esc cancels after the first press.

Captured responses

Named request responses kept for {{name.response.*}} references can be inspected through Captured responses in the command palette. The window lists the source file, request name, status, header count, and body size without exposing captured body or header values. Press c twice in the window to clear the current session's captured responses; Esc cancels after the first press. Subsequent references remain unresolved until those named requests are run again.

Exporting responses

While Producer is focused, y copies the current response body in the active Pretty/Raw mode. Y copies the complete response — status, headers, and body — without the pane's labels or colour markup. Clipboard export uses the terminal's clipboard support, which may need OSC 52 to be enabled in the terminal.

Press C to copy the request as a multiline POSIX-shell cURL command. The command uses the resolved URL, declared headers, encoded GraphQL payload, and body from the request. This explicit export includes real secret values, so treat the clipboard as sensitive. A multi-entry Hurl workflow cannot be represented as one cURL command. Requests restored from redacted on-disk history must be run once in the current session before they can be repeated or exported as cURL.

Press s to save the unformatted body, or S to save the same complete response that Y copies, including the body in the active Pretty/Raw mode. The path prompt suggests a name from the request and timestamp, using the content type for a body or -response.txt for a complete response; relative paths are resolved from the project root. New files and directories use private permissions, and an existing file requires a second Enter before it is overwritten. These actions operate on the entry currently selected with [ / ], apply the same secret redaction as the response pane, and report when the exported body was truncated.

Navigation

  • j / k or arrows: move and scroll.
  • Ctrl+d / Ctrl+u: move or scroll down/up by half a page.
  • Ctrl+f / Ctrl+b: move or scroll down/up by a full page.
  • gg / G: go to the first/last item or line.
  • zt / zz / zb: place the current item or scroll anchor at the top, centre, or bottom of the focused area.
  • Enter / l: select a file/request; Enter executes it from the Suite pane.
  • Esc: go back; in the response pane it also cancels the active run.
  • Ctrl+h/j/k/l: move between areas according to the following map:
Focused area Shortcut Destination
Files Ctrl+l Suites
Suites Ctrl+h Files
Suites Ctrl+j Suite
Suites Ctrl+l Producer
Suite Ctrl+h Files
Suite Ctrl+k Suites
Suite Ctrl+l Producer
Producer Ctrl+h Suite

While a stream is open its pane takes the place of Producer, so Ctrl+l from Suites or Suite leads to it and Ctrl+h leads back.

  • /: search in the focused Files, Suites, or Producer area; Enter finishes entering the query. Searchable panes show the current and total match count in their title, such as [2/7]; in Files and Producer, n / N move cyclically through matches.
  • r: reload the file tree in the background while Files is focused. Request file writes, creates, renames, and removals are also detected automatically.
  • p: toggle Pretty/Raw response bodies while Producer is focused. Pretty formats and highlights JSON, XML, and GraphQL; Raw shows exactly what came over the wire.
  • R: repeat the request currently shown in Producer; current-session History selections are supported.
  • y / Y: copy the current response body / complete response while Producer is focused.
  • C: copy the current request as an executable cURL command.
  • s / S: save the unformatted current response body / complete response while Producer is focused.
  • n / N: next/previous match in the focused Files or Producer area.
  • [ / ]: previous/next response history entry.
  • f: follow or pause the live frames while a stream pane is focused. Paused keeps the screen still while the connection keeps talking.
  • s: send a frame on the open connection; Up / Down in the composer recall frames sent earlier. An MQTT frame is published to the topic the request names.
  • c: clear the frame log without closing the connection.
  • History in the command palette: inspect the project's saved runs; use j / k, open one with Enter / l, or press c twice to clear all entries.
  • d: open parser, startup, and file-discovery diagnostics; press d, q, or Esc to close.
  • ?: open the built-in keyboard reference; press ?, q, or Esc to close.
  • : or Ctrl+p: open the command palette.
  • / in the command palette: filter commands as you type; Esc clears the filter before closing the palette.
  • Ctrl+r: reload ~/.config/lazyrest/config.yml without restarting.
  • q: close the active window; quit lazyrest when no window is open.
  • Ctrl+C: quit from anywhere.

Roadmap

The next improvements are ordered by risk and user impact. Completed work stays listed here until the next release so that the direction of the project remains visible.

  • Harden persistent history against secret leaks. Persist only an explicit allowlist of request and response fields, and redact GraphQL variables, GraphQL errors, and every other stored string that can contain a known secret.
  • Distinguish fatal parser errors from warnings. Keep useful diagnostics, but prevent a request from running when an external body is missing or another error makes the request unsafe to send.
  • Protect secrets created at runtime. Treat sensitive response references, such as tokens returned by a login request, as secrets and add a metadata-only history mode.
  • Surface history persistence failures. Report corrupted history files and background write failures in Diagnostics instead of silently ignoring them.
  • Fuzz the handwritten parsers. Cover request splitting, headers, variables, dotenv values, response references, redaction, and shell quoting with seeded fuzz tests.
  • Make releases transactional. Validate an explicit version on main, run checks and build archives before publishing its tag, and clean up a partial publication when GitHub Release creation fails.
  • Automate security checks. Run govulncheck in CI, monitor dependency updates, and attach provenance or an SBOM to release archives.
  • Add a headless runner. Allow named requests to run without the TUI so the same .http and .hurl files can be used in CI and scripts.
  • Refresh changed files automatically. Watch the project tree and reparse affected request files without requiring a manual reload.
  • Expand distribution. Add package-manager installation and notarized macOS builds after the release process and signing credentials are ready.

Development

go test ./...
go test -cover ./...       # package-level coverage
go test -race ./...        # what CI runs
go test ./ui -run TUI      # the terminal integration tests
go vet ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...
golangci-lint run ./...    # the set is pinned in .golangci.yml
go build ./...

The build must stay free of CGO, which CI checks by compiling every released target with CGO_ENABLED=0.

Seed corpora run as ordinary tests. CI also mutates every fuzz target briefly on pushes and pull requests, with a longer scheduled run each Monday. To investigate one target locally, use for example:

go test ./parser/http -run=^$ -fuzz='^FuzzHTTPDocumentSyntax$' -fuzztime=30s

To publish a release, first add and commit a dated ## [vX.Y.Z] section to CHANGELOG.md on main, then dispatch Go Tests and Release with that version:

gh workflow run go-test.yml --ref main -f version=vX.Y.Z

The workflow refuses an existing or non-increasing tag and a missing changelog section. It publishes the tag only after tests and all platform archives pass. Every archive contains a target-specific CycloneDX SBOM.cdx.json; the same SBOMs and their checksums are also attached separately to the GitHub Release. Dependabot checks Go modules and GitHub Actions for updates every Monday.

About

Terminal UI for the .http, .hurl and .socket requests in your project — live WebSocket, MQTT and STOMP streams, environment profiles, GraphQL, syntax-highlighted responses, and Vim-style navigation.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages