If you’ve ever used the Laravel PHP framework, you’re probably familiar with
the php artisan:tinker
command. It spins up a PHP REPL (read-evaluate-print loop) that lets you play around with the data in your database using the same
model code defined in your app. My team works almost exclusively in TypeScript
on Node.js, but many of us have backgrounds as Laravel devs, and we missed the
convenience of Tinker. When you’re first starting a project, it’s an easy way
to seed test data, and it can be much more convenient than firing up a client
to poke around your database. Our home-grown Node application didn’t have this
luxury, and I was curious what it would take to implement it. Turns out, it's
incredibly easy. You can write custom REPLs in Node in minutes.
The REPL API
I'm a fan of TypeScript, and I think the @types/node
package
makes this process even easier, but for the sake of simplicity, I'll stick to
vanilla JavaScript in these examples. The file for this example will be called
repl.js
. The first step is to import the built-in
repl
Node package.
const repl = require('repl');
Create a REPL instance with the start()
function. It accepts an
object with some basic customization options, including a prompt string (the
snippet of text that appears before each line of user input), color options,
options for evaluation strictness, enabling expression previews, and more (docs).
const loop = repl.start({
prompt: "~$ ",
ignoreUndefined: true,
breakEvalOnSigint: true, // allow exiting with ctrl-C during evaluation
});
Defining Globals
So far, we haven't accomplished anything that the REPL started by the
node
command wouldn't. The context
property on our
loop
object gives us one way to make the REPL more interesting.
If your app connects to a database, this would be a good place to add a
connection pool or an ORM client, like Prisma:
const mysql = require("mysql2");
const { PrismaClient } = require("@prisma/client");
loop.context.db = mysql.createConnection({
host: 'localhost',
user: 'root',
database: 'test'
});
loop.context.prisma = new PrismaClient();
This explicitly assigns the variables to the REPL's global scope. If you start
the REPL with node repl.js
, these variables are available
immediately:
[nate@rosemary repl-example]$ node repl.js
~$ db.query("SELECT count(id) FROM user");
~$ prisma.user.findMany({});
When connecting to a database, it's a good idea to add any needed teardown to
the uncaughtException
and SIGINT
process event
handlers to cleanup if the process crashes.
const teardown = () => {
loop.context.prisma.$disconnect();
loop.context.db.end();
};
process.on("uncaughtException", teardown);
process.on("SIGINT", teardown);
Custom Commands
You can add custom, dot-prefixed commands to your REPL with the
REPLServer.defineCommand
method. These functions are not part of
the global scope in the way that the .context
properties above
are, as those can be cleared with the .clear
command at runtime
by a user. Additionally, commands defined this way have useful help text that
appears as part of the output of .help
. It's worth noting that
these commands don't accept parameters like normal JS functions do, so if you
need parameterized commands, you'll have to attach them to the REPL context
like above.
loop.defineCommand('teardown', {
help: 'Tear down the database pool and Prisma connection.',
action: teardown,
});
These functions are accessible in the REPL like other global functions, but prefixed with a dot, and without the usual function-call parentheses.
[nate@rosemary repl-example]$ node repl.js
~$ .teardown
Builtins
All instances of the REPL class support a few dot-prefixed keywords that might
be familiar if you've ever run the base Node REPL with the
node
keyword in your shell. The most useful (and notable) of
these are .save
, .load
, .editor
, and
.exit
. The full list is available in the Node docs
here.
async/await
By default, like regular JS files, REPLs do not allow the use of
await
outside of an async function. To enable
await
at the prompt, use the
flag
--experimental-repl-await
when launching the REPL.
Other cool stuff to try
If you're interested in creating more advanced REPLs, I recommend checking out the full REPL API docs. Some points of interest are custom evaluator functions and customizing REPL output.