---
title: "Building a Developer CLI in Go: Architecture Decisions"
description: "Single binary, sub-50ms startup, cross-platform — here is our Go CLI architecture from command hierarchy and plugin system to config management and auto-updates."
canonical: https://nowah.xyz/blog/building-developer-cli-go-architecture
lastModified: "2026-08-07T08:09:49.813Z"
---

# Building a Developer CLI in Go: Architecture Decisions

Single binary, sub-50ms startup, cross-platform — here is our Go CLI architecture from command hierarchy and plugin system to config management and auto-updates.

The terminal is where developers think fastest. Context-switching to a browser, navigating to a dashboard, clicking through menus — each step breaks flow. A CLI that lets developers [search flights](/blog/launching-[tool-calling](/blog/tool-calling-at-scale-ai-travel-search)-layer-ai-agent-search-flights), check bookings, tail logs, and debug webhooks without leaving the terminal keeps them in the zone where they are most productive.

We built our CLI in Go. The choice was deliberate, and it shapes everything about the [developer experience](/blog/developer-experience-customer-experience).

## Why Go

![Illustration for this section](https://pics.nowah.xyz/website-media/developer-experience-016-img-1-command-tree.webp)

Four properties of Go made it the right choice for a developer CLI.

**Single binary distribution.** Go compiles to a single binary with no runtime dependencies. The developer downloads one file, runs it, and it works. No Python version conflicts. No Node.js installation required. No Java runtime to manage. This matters more than you might think — runtime dependency issues are one of the top reasons developers abandon CLI tools during installation.

**Fast startup.** Our CLI starts in under 50 milliseconds. Developers should never wait for a CLI to load. If there is a perceptible delay between hitting enter and seeing output, the tool feels sluggish. Go's compiled nature and fast process startup make sub-50ms startup achievable without heroic optimization.

**Cross-compilation.** Go cross-compiles trivially to macOS (ARM and Intel), Linux (AMD64 and ARM64), and Windows. One build step, six binaries. No platform-specific build infrastructure, no CI matrix complexity, no "works on my machine" issues.

**Mature TUI libraries.** The Go ecosystem has excellent terminal UI libraries for building interactive experiences — formatted tables, color output, progress bars, interactive prompts. We get rich terminal experiences without the overhead of a TUI framework in a higher-level language.

## Command hierarchy

The CLI command structure mirrors the API resource structure:

```
nowah auth login
nowah auth status
nowah search flights
nowah search hotels
nowah bookings list
nowah bookings get <id>
nowah sandbox start
nowah webhooks listen
nowah logs tail
nowah keys list
nowah keys rotate <id>
```

The pattern is \`nowah <resource\> <action\>\`\. This mirrors how the REST API is structured — \`nowah search flights\` maps to \`GET /flights/search\`, \`nowah bookings get bkg\_abc123\` maps to \`GET /bookings/bkg\_abc123\`\. Developers who know the API can guess the CLI commands, and vice versa\.

Each top\-level command group \(\`auth\`, \`search\`, \`bookings\`, \`sandbox\`, \`webhooks\`, \`logs\`, \`keys\`\) corresponds to a separate Go package\. This keeps the codebase organized and makes it easy for new contributors to find the code for a specific command\.

We avoided deep nesting\. No \`nowah travel flights search round\-trip economy\`\. Flat is better than nested for CLIs because developers remember short commands and tab\-complete them faster\.

## Configuration management

![Supporting diagram](https://pics.nowah.xyz/website-media/developer-experience-016-img-2-config-layers.webp)

Configuration resolves through four layers, from lowest to highest priority:

**Defaults** are compiled into the binary. The default API endpoint, output format (table), and color mode (auto-detect) are always available even with no configuration file.

**Config file** lives at \`~/\.config/nowah/config\.yaml\`\. It stores persistent settings like the default API endpoint, preferred output format, and active authentication profile\. The file is created automatically on first run with sensible defaults\.

**Environment variables** override config file values\. \`NOWAH\_API\_URL\`, \`NOWAH\_API\_KEY\`, \`NOWAH\_OUTPUT\_FORMAT\` — each config value has a corresponding environment variable\. This is essential for CI/CD environments where you cannot write to the config file\.

**Flags** override everything\. \`\-\-format json\`, \`\-\-api\-key nwh\_\.\.\.\`, \`\-\-endpoint https://staging\.api\.nowah\.com\`\. Flags are for one\-off overrides that should not persist\.

This layering means the CLI works out of the box with zero configuration, adapts to team conventions via config files, supports CI environments via environment variables, and allows ad-hoc overrides via flags. Each layer serves a different use case, and the priority order is what developers expect.

## Plugin architecture

The core CLI handles authentication, configuration, and HTTP transport. Domain-specific features — flight search rendering, booking state visualization, webhook inspection — are implemented as internal plugins that share the core infrastructure.

Each plugin registers its commands during CLI initialization. The plugin interface is simple: provide a command tree and a set of command handlers. The core CLI handles argument parsing, configuration resolution, and HTTP client setup. The plugin handles the domain logic and output formatting.

This architecture lets us ship all plugins in the same binary (no separate downloads or installation steps) while keeping the code modular. Adding a new command group means creating a new plugin package and registering it at startup.

We considered supporting external plugins (separate binaries that the CLI discovers and invokes) but decided against it for now. External plugins add distribution complexity and version management overhead that we do not want to impose on developers. If a developer needs functionality beyond what the CLI provides, they can use the API directly or compose CLI commands with shell scripting.

## Auto-update mechanism

The CLI checks for updates on every startup, but the check runs in a background goroutine and never blocks execution. The developer runs their command, gets their output, and if a new version is available, a single line prints after the output:

```
A new version of nowah is available (v2.4.0). Run 'nowah update' to upgrade.
```

The check result is cached for 24 hours so developers running fifty commands a day only hit the update server once. The cache is a timestamp file in the config directory.

\`nowah update\` downloads the correct binary for the current OS and architecture, verifies the checksum, replaces the current binary, and prints the changelog summary\. It includes a rollback option in case the update causes issues\.

For CI environments, version pinning is available via the \`NOWAH\_CLI\_VERSION\` environment variable\. When set, the CLI skips update checks and the auto\-updater refuses to change the version\. This ensures reproducible builds across team members and CI runs\.

## Lessons learned

After a year of CLI development, a few things we would emphasize to anyone building a developer CLI:

**Startup time matters more than you think.** We measured, and developers notice delays above 100 milliseconds. We got to under 50 milliseconds by deferring expensive initialization (network calls, large file reads) until the specific command that needs them runs.

**Output format consistency is trust.** If \`\-\-format json\` on one command produces a slightly different wrapper structure than on another, developers lose confidence in scripting against the output\. We test JSON output structure in CI for every command\.

**\[Error messages\]\(/blog/error\-messages\-ai\-agent\-lifeline\) in the terminal need more context than API errors\.** In a terminal, the developer does not have a network inspector or response headers to look at. The CLI [error message](/blog/anatomy-of-perfect-error-message) has to stand on its own with enough context to diagnose the problem.

**Testing CLIs is harder than testing APIs.** We test by capturing stdout and stderr as strings and asserting against them. This is brittle but necessary. We also test the JSON output programmatically, which is more robust.

The CLI is not a secondary interface to the API. For many developers, it is the primary one. We treat it with the same care and investment as the API itself.

---

Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. [Plan your next trip](https://app.nowah.xyz).
