# 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.

Source: https://voxgig.com/howto/preload-a-generated-client-into-a-node-repl

- Audience: api-consumer
- Level: beginner
- Languages: typescript, javascript
- Verified: 2026-09-14
- Published: 2026-09-14
- Updated: 2026-09-24

## 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](https://nodejs.org/api/repl.html#replserverdefinecommandkeyword-cmd), and awaiting a promise
at the prompt works without a flag in current Node.[^1] The [REPL
options](https://nodejs.org/api/repl.html#replstartoptions) 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

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A REPL bootstrap you write](https://nodejs.org/api/repl.html#replstartoptions) | Any client, and you want to choose the names and what help says | Twenty lines to maintain, and a file every contributor has to know about | The client already ships a REPL |
| [An sdkgen-generated REPL target](https://github.com/voxgig/sdkgen) | You already generate the client and want the REPL to follow the model | Names and layout decided by the generator rather than by you | You want the REPL to expose your own helpers alongside |
| [node --require with a preload](https://nodejs.org/api/cli.html#-r---require-module) | You want the plain `node` command to gain the context | A hook that applies to every process started that way, not only the REPL | The preload should apply to an explicit command |
| [openapi-fetch in a plain REPL](https://openapi-ts.dev/openapi-fetch/) | A typed client where paths rather than entities are the interface | Typing the path on every call, which is more keystrokes than tab completion | You 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.

```js title="repl.mjs"
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.

## Print what was preloaded

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

```js title="repl.mjs"
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.

```bash
METERCO_TOKEN=sk_live_demo node repl.mjs < session.txt
```

```text output
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.

```bash
node --test repl.test.mjs
```

```text output
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.

## Related how-tos

- [Turn an exploratory REPL session into a script](/howto/save-a-repl-session-as-a-script)

- [Add a bearer token to fetch without a client library](/howto/bearer-token-fetch-wrapper)

## Last verified

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

[^1]: The flag is the off switch. [`--no-experimental-repl-await`](https://nodejs.org/api/cli.html)
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.

[^2]: The demo token borrows a convention. [Stripe's keys](https://docs.stripe.com/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.

[^3]: [`repl.start`](https://nodejs.org/api/repl.html#replstartoptions) 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.