Read console input with Node.js

When people first learn a programming language, they often start by creating small programs that run in the terminal (a.k.a. "console" a.k.a. "command prompt"). The program will give some text output, then the user will type some text input, then the program will read it and give some more output.

One such exercise could be: create a program that asks the user for their name, allows them to type their name, then greets them by name.

There are many programming tutorials that teach Python and Ruby and other scripting languages, and many of them start here, with simple console programs. On the other hand, Node.js tutorials usually start by creating a simple web server. That's super cool, but it means most Node.js developers aren't very familiar with how the terminal works in their own programming language.

Interacting with the terminal

First, we need a way to write to the terminal and also a way to read data that was entered in the terminal:

  • We will use process.stdin to read data that was typed in the terminal.
  • We will use process.stdout to write data out to the console.

stdin is the standard input stream, and stdout is the standard output stream. This pattern exists across programming languages. To simplify, these streams allow data to pass in and out of the program through the terminal.

Here's a one-liner example of how to write to stdout:

process.stdout.write('Hello world!')

And here's a simple example of how to read from stdin:

process.stdin.once('data', data => {
  console.log('Data entered: ' + data)
  process.exit()
})

We added a one-time event listener for a 'data' event from the stream, which happens when someone types input and presses Enter.

Since stdin is a Readable stream, it begins in paused mode. Adding a 'data' listener switches the stream to flowing mode. When stdin is in flowing mode, it listens for data from the terminal.

Once the 'data' event triggers, we can work with the input from the console. In this case, we will just log some output to the console. Notice how the global console instance is already configured to write to stdout!

Also notice how we had to call process.exit() to exit the program. Since we left the process.stdin stream open, the program was still listening for user input. Calling process.exit() is one way to terminate the program. A more proper approach would be to close the stream by calling process.stdin.pause() in the same place.

Keeping things organized

Since Node.js is asynchronous, things are going to get a little weird. When you ask the user for input, Node.js doesn't wait for them to type something. It just keeps on working. So it's important that we keep things as organized as possible so we don't lose track of what's happening.

Fortunately, the built-in readline module simplifies things for us:

const readline = require('readline')const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})
​
rl.question('What is your name?', nameAnswer => {
  console.log(`Nice to meet you, ${nameAnswer}.`)
})

Once we create a readline interface, we don't have to worry about using process.stdin and process.stdout directly.

Notice that readline uses callbacks. If we want to ask several questions in sequence, we have to juggle those callbacks somehow:

rl.question('What is your favorite color?', colorAnswer => {
  console.log(`I like ${colorAnswer} too.`)
​
  rl.question(`What shade of ${colorAnswer} is best?`, shadeAnswer => {
    console.log(`Wow, ${shadeAnswer} is also my favorite!`)
    
    rl.close()
  })
})

Of course, JavaScript has a solution for this. We can use Promises to avoid nesting callbacks ad nauseam:

const question = prompt => {
  return new Promise((resolve, reject) => {
    rl.question(prompt + '\n', resolve)
  })
}

Conveniently, we have abstracted away all interaction with the standard input and output streams. That leaves you to focus on the higher-level concern of what text should display and what to do with the text typed by the user. We have also abstracted away the pyramid of callbacks, so we can more easily see line-by-line how the terminal interaction will go.

Putting it all together

Here's a final complete example of our setup, which is inside an async function so we can use await to inline our Promises:

const readline = require('readline')const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})const question = prompt => {
  return new Promise((resolve, reject) => {
    rl.question(prompt, resolve)
  })
}

(async () => {
  const nameAnswer = await question('What is your name?')
  console.log(`Nice to meet you, ${nameAnswer}.`)
  
  const whereAnswer = await question('Where are you from?')
  console.log(`I hear it's nice in ${whereAnswer}.`)
  
  rl.close()
})()