How-to › Ship a CLI or REPL

How to preload a generated client into a Node REPL#

Start a REPL with the client already built and its entities named, so exploring an API is one command rather than six lines of setup typed from memory.

Audience
API consumer
Level
beginner
Topic
Explore an API interactively
Languages
TypeScript and JavaScript
Verified

Every time you want to poke at an API you open a REPL and type the same four lines. The import, the token, the client construction, and then a typo in the entity name. The exploration you wanted to do takes a minute to reach, and half the time the token came from the wrong environment because you pasted it from a terminal scrollback.

What you get

You will end up with one command that opens a REPL with the client built, the entities named, and a help() that lists them. Tab completion then works from the first keystroke. This is for you if you explore an API often enough to have retyped the setup.

Short answer

Start node:repl yourself and put the built client on its context, with each entity named at the top level so tab completion offers the operations. Define the names as non-configurable, so a stray assignment cannot replace one. Add a help() that prints what was preloaded, because nothing else tells a newcomer what is in scope.

You will need

Node 22 or later, and a client library you can import. The context is the REPL’s own object, and awaiting a promise at the prompt works without a flag in current Node.1 The REPL options are where the input, the output, and the global-context decision are made.

Voxgig maintains sdkgen, whose REPL is one of the six surfaces it generates. This page compares that with a bootstrap you write and with a plain REPL over a typed fetch client.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
A REPL bootstrap you writeAny client, and you want to choose the names and what help saysTwenty lines to maintain, and a file every contributor has to know aboutThe client already ships a REPL
An sdkgen-generated REPL targetYou already generate the client and want the REPL to follow the modelNames and layout decided by the generator rather than by youYou want the REPL to expose your own helpers alongside
node —require with a preloadYou want the plain node command to gain the contextA hook that applies to every process started that way, not only the REPLThe preload should apply to an explicit command
openapi-fetch in a plain REPLA typed client where paths rather than entities are the interfaceTyping the path on every call, which is more keystrokes than tab completionYou prefer entities with operations on them

The choice is really between naming things yourself and accepting what a generator named. A generated REPL is free and consistent across languages, and the names in it follow the model. A bootstrap you write costs twenty lines and lets you add the two helpers your team actually reaches for, such as a pretty printer, or a fixture loader.

Whichever you pick, put the entities at the top level rather than behind the client object. Typing api.meter.list() is three tab completions and meter.list() is one, and over a session of exploration that difference is most of the value.

Name the entities on the context

Assign each one rather than only the client.

const context = {
  api: client,
  meter: client.meter,
  invoice: client.invoice,

Keep api as well. A reader who wants to see the whole shape types Object.keys(api), and a helper that takes the client wants the client rather than one entity.

Make the names non-configurable, which the sample does with defineProperty. A REPL session is full of half-typed assignments, and meter = 1 at the wrong moment otherwise ends the session’s usefulness without explaining why.

A help() that writes rather than returns is the difference between readable and noisy.

help: () => {
  output.write('api            the whole client\n')
  output.write('meter, invoice entities, each with list, load and create\n')
  output.write('token          the credential the client was built with\n')
},

Returning the text instead would print it as an escaped JavaScript string, with the newlines visible as characters. Writing to the output stream prints what a person expects and returns undefined, which the REPL shows and nobody minds.

Take the token from the environment, with the variable name in the help. A REPL that silently uses a credential from somewhere is a REPL that eventually talks to production.2

Check it worked

Feed a session into the REPL and read what it answered.

METERCO_TOKEN=sk_live_demo node repl.mjs < session.txt
api            the whole client
meter, invoice entities, each with list, load and create
token          the credential the client was built with
undefined
'sk_live_demo'
[ 'token', 'meter', 'invoice' ]
[ 'list', 'load', 'create' ]
[
  { id: 'mtr_8f2', serial: 'SN-40199', state: 'installed' },
  { id: 'mtr_31a', serial: 'SN-40200', state: 'retired' }
]
{ id: 'mtr_8f2', serial: 'SN-40199', state: 'installed' }
null

The two Object.keys lines are what tab completion is reading. Typing meter. at a real prompt offers list, load and create, because they are properties of a named object rather than strings inside a client somewhere.

The last two lines are one call that found a record and one that did not. Piping a file into the REPL is also how you test a bootstrap without a terminal, which is what the tests below do.

The undefined on the fourth line is worth noticing rather than hiding. help() printed three lines and returned nothing, and the REPL says so. A help that returned its text instead would print the whole thing as one escaped string.

node --test repl.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 201.709538

When it goes wrong

Tab completion offers nothing. The client is behind a promise, or the context was set before the REPL started. Assign after start returns, and build the client before you assign it. Anything asynchronous belongs in the bootstrap rather than on the context.

An await at the prompt returns a pending promise. An older Node, or a REPL started with the flag off. Check the version before rewriting anything.

The second REPL in one process fails to start. The bootstrap used the real global as its context. Give each server its own context, which also stops a session leaking names into the next one.3

The REPL starts silently with the wrong token. The bootstrap fell back to a default. Print the token’s source in the help, and refuse to start if the variable is unset, which is why the command in the previous section sets it. A default is worse than a crash here. A misspelled variable name gives you a REPL that starts, looks right, and fails on its first call. A REPL that cannot say where its credential came from is one nobody should point at a live account.

When not to do this

Do not preload a client that talks to production by default. An exploratory REPL invites experiments, and a create typed to see what it does is a record somebody has to delete.

Do not add helpers until you have typed the same thing three times. A bootstrap full of conveniences nobody uses is a file people are afraid to change, and a bootstrap nobody edits stops matching the client within a release or two.

Do not treat the sdkgen REPL target as a reason to skip the bootstrap question. It gives you a consistent REPL per language, generated from the model, and the helpers your own team keeps retyping are still yours to add.

Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

Footnotes

  1. The flag is the off switch. --no-experimental-repl-await was added in v16.6.0 and exists to turn off top-level await in the REPL. That is the form a feature takes once it is on by default. The option that survives is the one that turns it off, and this one has kept the word experimental in its name. ↩︎ Back to text

  2. The demo token borrows a convention. Stripe’s keys begin with sk_test_ in a sandbox and sk_live_ in live mode, with pk_ and rk_ for publishable and restricted keys on the same pattern. The first eight characters say which money a request touches. The prefix answers the question this section asks before the help does, provided somebody reads it. ↩︎ Back to text

  3. repl.start takes a useGlobal option, false by default, so each server gets a separate context unless asked otherwise. The documentation adds that the node command’s own REPL sets it to true. The interactive REPL everybody knows is therefore the one configuration the library does not default to, which is a thing to know before copying its behavior. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.