Chapter 36: Node.js Readline Module
1. What is the readline module and why do we need it?
readline is a built-in core module that lets you read input from a readable stream line by line — most commonly from the terminal (process.stdin).
In simple words:
It turns the stream of characters a user types into the terminal into clean, usable lines (one line at a time).
Without readline, reading from stdin is painful:
- You get raw chunks (may split in the middle of a line)
- You have to handle \n, \r\n, backspace, arrow keys yourself
- No built-in history, tab completion, prompt, etc.
readline solves all of this.
Most common real-world uses (2025–2026):
- CLI tools (like npm init, create-react-app, vite, prisma, turbo, eslint –init)
- Interactive scripts (setup wizards, configuration helpers)
- REPLs (Read-Eval-Print Loops) — like the node REPL itself
- Command-line chat bots, debug tools, admin consoles
- Reading large text files line-by-line (memory efficient)
2. Modern import style
|
0 1 2 3 4 5 6 7 8 9 10 |
// Recommended 2025–2026 style import * as readline from 'node:readline'; // or named imports (very common) import { createInterface, Interface } from 'node:readline'; |
You usually don’t need to import readline/promises — the main API is callback-based, but we can make it promise-friendly easily.
3. The most important way: readline.createInterface()
Almost every use of readline starts with creating an interface.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import * as readline from 'node:readline'; import process from 'node:process'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, // optional but very useful prompt: 'my-app> ', // custom prompt completer: myTabCompleter, // tab completion (later) historySize: 1000, // how many previous commands to remember terminal: true // true in real terminal, false in tests }); |
Very important options explained:
| Option | Default | Typical value in real projects | Meaning / When to change |
|---|---|---|---|
| input | process.stdin | usually leave as is | Where to read characters from |
| output | process.stdout | usually leave as is | Where to write prompt & echoed text |
| prompt | ‘> ‘ | ‘λ ‘, ‘→ ‘, ‘app> ‘, ‘configurator $ ‘ | What appears before each line |
| historySize | 30 | 200–2000 | How many previous commands to keep in memory |
| completer | null | custom function | Tab completion logic |
| terminal | true | false in tests, true in real terminal | Enables colors, cursor control, etc. |
4. Basic usage – ask user questions one by one
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import * as readline from 'node:readline'; import process from 'node:process'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'my-app> ' }); // Show prompt rl.prompt(); rl.on('line', (line) => { const input = line.trim(); switch (input.toLowerCase()) { case 'hello': console.log('Hello there! 👋'); break; case 'exit': console.log('Goodbye!'); rl.close(); break; default: console.log(`You typed: "${input}"`); } rl.prompt(); // show prompt again }); rl.on('close', () => { console.log('\nSession ended.'); process.exit(0); }); |
Run this file → you get an interactive prompt:
|
0 1 2 3 4 5 6 7 8 9 10 |
my-app> hello Hello there! 👋 my-app> exit Goodbye! Session ended. |
This is the foundation of every CLI tool.
5. Most useful pattern: Ask questions sequentially
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import * as readline from 'node:readline'; import process from 'node:process'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); function askQuestion(query) { return new Promise((resolve) => { rl.question(query, (answer) => { resolve(answer.trim()); }); }); } async function setupWizard() { console.log('Welcome to MyApp setup wizard!\n'); const name = await askQuestion('What is your name? '); const email = await askQuestion('Your email address? '); const projectName = await askQuestion('Project name? '); console.log('\nSummary:'); console.log(` Name: ${name}`); console.log(` Email: ${email}`); console.log(` Project: ${projectName}`); rl.close(); } setupWizard().catch(console.error); |
Output:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Welcome to MyApp setup wizard! What is your name? Aman Your email address? aman@example.com Project name? task-manager Summary: Name: Aman Email: aman@example.com Project: task-manager |
This pattern is used by almost every modern CLI tool:
- npm init
- prisma init
- create-vite
- npx create-next-app
- turbo gen
6. Tab completion (very impressive feature)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import * as readline from 'node:readline'; import process from 'node:process'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer: (line) => { const completions = ['help', 'hello', 'status', 'exit', 'version']; const hits = completions.filter(c => c.startsWith(line)); return [hits.length ? hits : completions, line]; }, prompt: 'cli> ' }); rl.prompt(); rl.on('line', (line) => { const cmd = line.trim(); if (cmd === 'hello') console.log('Hi there!'); if (cmd === 'exit') rl.close(); rl.prompt(); }); |
Now when you press Tab after typing he, it completes to hello.
Real-world completer example – file paths
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function fileCompleter(line) { const path = require('node:path'); const fs = require('node:fs'); let dir = '.'; let partial = line; if (line.includes('/')) { dir = path.dirname(line); partial = path.basename(line); } try { const files = fs.readdirSync(dir); const hits = files.filter(f => f.startsWith(partial)); return [hits, partial]; } catch { return [[], partial]; } } rl.completer = fileCompleter; |
Now you can type read conf + Tab → config.json completes!
7. Common mistakes & traps
| Mistake | Consequence | Fix / Best Practice |
|---|---|---|
| Forgetting to call rl.prompt() again | No new prompt appears after answer | Always call rl.prompt() after handling line |
| Not handling ‘close’ event | Program hangs after Ctrl+C | Always add rl.on(‘close’, …) |
| Using rl.question without promise wrapper | Callback hell | Wrap in promise or use readline/promises |
| Not closing interface | Terminal stays in raw mode | Call rl.close() when done |
| Not handling empty lines / whitespace | Unexpected behavior | Use .trim() on input |
8. Modern helper – promise-based question
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import * as readline from 'node:readline/promises'; import process from 'node:process'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const name = await rl.question('What is your name? '); const age = await rl.question('How old are you? '); console.log(`Hello ${name}, you are ${age} years old!`); rl.close(); } main().catch(console.error); |
Note: readline/promises is available since Node.js 17+ — highly recommended for new code.
Summary – Quick cheat sheet 2025–2026
| You want to… | Recommended way | Typical real-world example |
|---|---|---|
| Ask one question | rl.question() or await rl.question() | Setup wizard, configuration script |
| Build interactive CLI | rl.on(‘line’, …) + rl.prompt() | REPL, admin console, debug tool |
| Add tab completion | completer function | File paths, commands, options |
| Read line-by-line from file/stream | readline.createInterface({input: fs.createReadStream(…)}) | Process large log/CSV/JSONL files |
| Modern promise-based API | import * as readline from ‘node:readline/promises’ | Clean async/await CLI flows |
Which direction would you like to go much deeper next?
- Building a full-featured interactive CLI (commands, help, history, tab completion)
- Reading very large files line-by-line using readline
- Custom completers (commands, files, dynamic options)
- Testing interactive CLIs (mocking stdin)
- Graceful shutdown & cleanup patterns
Just tell me what interests you most — I’ll continue with detailed, production-ready examples. 😊
