Ship your API as a CLI

gofastr generate cli turns your app's HTTP API into a branded terminal
client you distribute to your customers — the stripe/gh experience
for the app you built. The output is a standalone, stdlib-only
package main that imports exactly one thing: your generated
entities/client package. Customers authenticate with a scoped API
token minted in your app, and every entity gets the full set of CRUD
operations plus batch operations and a live event stream.

3 lines
cd myapp           # the directory holding entities/gofastr generate cligo build ./cmd/myapp

The generator reads the entity set from your project source
(entities/*.go, the same recovery machinery gofastr pack trusts) —
no blueprint or YAML involved. It needs an enclosing go.mod to derive
the client import path.

The output is pure stdlib Go, so it builds for every platform Go
targets — cross-compile releases with GOOS/GOARCH
(GOOS=windows GOARCH=amd64 go build ./cmd/myapp), and the config file
lands in each OS's native config dir (os.UserConfigDir). The default
output directory is cmd/<binary>/, the standard installable-main
layout: if your module is public, customers can install directly with

1 lines
go install your.module/path/cmd/myapp@latest

For closed-source apps, distribute the cross-compiled binaries.

What your customers get

12 lines
myapp login --url https://app.example.com --with-token   # paste a token oncemyapp posts list --published true --sort -created_at -o tablemyapp posts list --views-gt 100 -q searchterm --include authormyapp posts get 42myapp posts create --title "Hello" --views 3myapp posts create --json @post.jsonmyapp posts patch 42 --published=false     # explicit zeros survivemyapp posts update 42 --json '{"title":"Edited"}'myapp posts batch-create --json @rows.json # atomic, up to 100myapp posts batch-delete id1 id2 id3myapp posts watch                          # live SSE feed, one JSON line/eventmyapp posts delete 42

Help is built in at every level, always exiting 0: bare myapp (or
help/--help) lists every command, a bare entity command
(myapp posts) lists that entity's subcommands, and --help on any
verb prints its full flag reference. An unknown subcommand prints the
group usage and exits 2. version prints the binary name.

Per verb:

  • list — an equality filter flag per field (--status active,
    comma list = IN), range flags (--views-gt/-gte/-lt/-lte) on
    numeric/date/timestamp fields, --<field>-like on text fields,
    plus --sort, --page/--limit, --cursor (keyset), --include,
    --fields, and --param k=v (repeatable) as the escape hatch for
    anything else. -q appears only when the entity declares
    SearchFields; --trashed only with SoftDelete. -o table
    renders an aligned table instead of JSON.
  • create / update / patch — per-field flags OR --json
    (inline, @file, or - for stdin); the two are mutually exclusive.
    Field flags are presence-faithful: only explicitly-set flags enter
    the body (via flag.FlagSet.Visit), so --published=false really
    sends false — the payload is built as a raw map, never squeezed
    through a struct with omitempty.
  • batch-create / batch-update--json with an array, sent
    through the atomic _batch routes; the {committed, results[]}
    envelope prints verbatim and a rollback exits 1.
  • watch — subscribes to GET {entity}/_events and prints one
    {"event":…,"data":…} line per server event until interrupted.
    No auto-reconnect: wrap it in a shell loop if you need one.

Connection resolution, in order: --url/--token flags →
<BINARY>_URL/<BINARY>_TOKEN env vars → the config file written by
login (<user-config-dir>/<binary>/config.json, 0600). Exit codes:
0 success, 1 API/transport error (including a rolled-back batch),
2 usage, 4 authentication failure (401/403).

Auth: scoped API tokens

The CLI sends Authorization: Bearer gfsk_… on every request — wire
auth.TokenMiddleware
alongside your session middleware, mount auth.NewTokensPlugin so
logged-in users can mint their own scoped tokens in the app, and add
auth.RequireAPIScopes("/api") so those scopes are actually enforced
(a customers:* token gets 403 off every other resource). Bearer
requests bypass both CSRF layers by design, so the CLI needs no cookie
or CSRF handling. The customer flow:

  1. Log in to your app in a browser.
  2. Mint a scoped token (POST /auth/tokens, plaintext shown once).
  3. echo "$TOKEN" | myapp login --url https://app.example.com --with-token.

--with-token reads stdin so the token never echoes or lands in shell
history; the interactive prompt warns that input echoes (the stdlib has
no termios). logout deletes the stored token — revoke it in the app
to invalidate it server-side.

Choosing what to expose

Everything is generated by default. Narrow it declaratively:

4 lines
gofastr generate cli --only=posts,comments        # entity allow-listgofastr generate cli --exclude=audit_logs         # entity deny-listgofastr generate cli --verbs=list,get             # read-only CLIgofastr generate cli --verbs='posts=list,get;comments=*'

Selection names match the entity name, table, or kebab command form; a
typo fails generation rather than silently generating everything.
Verbs: list get create update patch delete batch-create batch-update batch-delete watch. Excluded entities and verbs render nothing — there
is no dead code to strip. The chosen selection is echoed in the
generated main.go header so a later --force regen can reproduce it.

Other flags: --out=cmd/<binary> (target directory), --binary=<name> (command
name; defaults to the project directory, and drives the env-var
prefix), --api-prefix=api (must match your AppConfig.APIPrefix
it's baked into the client's base URL so customers pass a bare server
URL), --dry-run, --json.

Extending and regenerating

Generation is one-shot owned code: re-running refuses to overwrite and
--force regenerates — except custom.go, which is only ever
created when absent
. That file is the extension seam:

  • customCommands() is merged over the generated command table; an
    entry with a generated name ("posts list") replaces that command,
    a new name adds one. Wrap rather than replace by calling the
    generated run function (runPostsList(...)) from your own.
  • configureClient(c *client.Client) runs before every request-bearing
    command — install a custom http.Transport, default headers, retries.
  • Custom server endpoints are reachable via the client's raw
    Do(ctx, method, path, body, out) escape hatch.

Fields with hidden: true never appear; read_only fields appear as
list filters but not as mutation flags. Image/file fields are excluded
entirely — the generated client doesn't speak multipart yet. A field
whose name collides with a reserved flag (sort, page, json,
url, …) fails generation with the entity and field named: rename the
field or exclude the entity.

Common mistakes

  • Mismatched --api-prefix. The prefix is baked into the CLI at
    generate time. If your app sets AppConfig.APIPrefix: "/api" but you
    generate with --api-prefix="" (or vice versa), every command 404s.
    Match them, and regenerate after changing the app's prefix.
  • No token middleware on the server. The CLI authenticates with
    gfsk_ bearer tokens; without auth.TokenMiddleware mounted those
    requests arrive anonymous and secure-by-default CRUD returns 401
    (CLI exit code 4). Mount it alongside SessionMiddleware and give
    users TokensPlugin to mint tokens.
  • Minting scopes without enforcing them. TokenMiddleware alone
    authenticates the token's owner everywhere; the scope list is only
    enforced where you gate it. Mount auth.RequireAPIScopes("/api")
    (or per-route RequireScope) or a customers:* token silently
    carries the user's full capability.
  • Editing generated files, then regenerating with --force. Only
    custom.go survives a forced regen. Put every customization there
    (override, wrap, or add commands); treat the other files as
    regenerable.
  • Expecting --published false to parse. Bool field flags follow
    stdlib flag semantics: bare --published sets true, and the
    explicit-value form needs =--published=false. The space form
    treats false as a positional and stops flag parsing.

Sibling: SDKs

gofastr generate sdk is the library twin of this command — a
downloadable Go module and JS/TS client for the same API, hosted by the
app itself behind a live docs site. See sdk.