Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Readline no Node.js: Guia Prático

Atualizado em: 8 de agosto de 2026

Terminal Linux usado em desenvolvimento e administração de aplicações Node.js

Ferramentas de linha de comando precisam ler respostas do usuário, exibir prompts, oferecer autocomplete e processar arquivos de texto linha por linha. O módulo Readline no Node.js, disponível como node:readline, conecta streams de entrada e saída para criar interfaces interativas sem dependências externas.

Embora seja simples fazer uma pergunta, aplicações reais precisam tratar encerramento, Ctrl+C, histórico, dados inválidos, saída não interativa e arquivos grandes. Também é importante não bloquear o event loop nem acumular todo o conteúdo em memória.

Neste guia, você aprenderá a criar interfaces, usar a API de Promises, validar respostas, implementar autocomplete, ocultar dados sensíveis, processar arquivos linha por linha e testar CLIs.

Criando a primeira interface

const readline = require('node:readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.question('Qual é o seu nome? ', answer => {
  console.log(`Olá, ${answer}!`);
  rl.close();
});

process.stdin é uma Readable Stream e process.stdout é uma Writable Stream. A interface mantém o processo aberto até ser fechada.

A documentação oficial de Readline apresenta interfaces, eventos e métodos. A documentação de Process explica stdin, stdout e sinais.

Para aprofundar o fluxo de dados, consulte Streams no Node.js e o que é Node.js.

API de Promises

const readline = require('node:readline/promises');
const { stdin: input, stdout: output } = require('node:process');

async function main() {
  const rl = readline.createInterface({ input, output });

  try {
    const name = await rl.question('Nome: ');
    console.log(`Bem-vindo, ${name}`);
  } finally {
    rl.close();
  }
}

main().catch(console.error);

O bloco finally garante o fechamento mesmo quando a validação ou outra etapa falha.

Fazendo várias perguntas

async function collectSettings() {
  const rl = readline.createInterface({ input, output });

  try {
    const host = await rl.question('Host: ');
    const port = await rl.question('Porta: ');
    const secure = await rl.question('Usar TLS? [s/N] ');

    return {
      host: host.trim(),
      port: Number(port),
      secure: secure.trim().toLowerCase() === 's'
    };
  } finally {
    rl.close();
  }
}

Todas as respostas chegam como strings. Converta e valide antes de usar.

Validação com repetição

async function askPort(rl) {
  while (true) {
    const raw = await rl.question('Porta [3000]: ');
    const value = raw.trim() || '3000';
    const port = Number(value);

    if (Number.isInteger(port) && port > 0 && port <= 65535) {
      return port;
    }

    console.log('Informe uma porta entre 1 e 65535.');
  }
}

Defina também uma forma de cancelar. Um loop que não aceita saída torna a ferramenta frustrante.

Evento line

Para processar várias linhas digitadas:

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  prompt: '> '
});

rl.prompt();

rl.on('line', line => {
  const command = line.trim();

  if (command === 'exit') {
    rl.close();
    return;
  }

  console.log(`Comando recebido: ${command}`);
  rl.prompt();
});

O evento é emitido quando a entrada encontra um fim de linha. A sequência depende da plataforma, mas o módulo normaliza o comportamento para o consumidor.

Evento close

rl.on('close', () => {
  console.log('Interface encerrada');
});

O evento pode ocorrer após rl.close(), fim da entrada ou sinais tratados. Não tente continuar fazendo perguntas depois dele.

Tratando Ctrl+C

rl.on('SIGINT', () => {
  rl.question('Deseja realmente sair? [s/N] ', answer => {
    if (answer.trim().toLowerCase() === 's') {
      rl.close();
    } else {
      rl.prompt();
    }
  });
});

Quando não há listener de SIGINT na interface, o comportamento padrão pode encerrar ou propagar o sinal conforme o ambiente. Para aplicações com recursos abertos, integre ao Graceful Shutdown no Node.js.

Autocomplete

const commands = [
  'help',
  'status',
  'start',
  'stop',
  'exit'
];

function completer(line) {
  const hits = commands.filter(command => command.startsWith(line));
  return [hits.length ? hits : commands, line];
}

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  completer
});

O completer deve responder rapidamente. Não faça uma consulta de rede lenta a cada tecla. Para fontes dinâmicas, mantenha um cache local atualizado.

Histórico

A interface interativa mantém histórico em memória. Opções como historySize, removeHistoryDuplicates e histórico inicial ajudam a controlar o comportamento:

const rl = readline.createInterface({
  input,
  output,
  historySize: 100,
  removeHistoryDuplicates: true
});

Não armazene senhas, tokens ou comandos sensíveis no histórico. Se a aplicação persiste histórico em arquivo, proteja permissões e permita desativá-lo.

Detectando terminal interativo

if (!process.stdin.isTTY || !process.stdout.isTTY) {
  console.error('Este comando exige um terminal interativo.');
  process.exitCode = 1;
}

Em pipelines e CI, stdin pode vir de arquivo e stdout pode ser redirecionado. Uma CLI bem projetada oferece flags não interativas para automação.

Modo interativo e modo automático

async function resolveConfig(args) {
  if (args.yes) {
    return {
      port: args.port || 3000,
      confirmed: true
    };
  }

  return collectSettings();
}

Não exija perguntas quando todos os valores podem ser fornecidos por flags ou variáveis de ambiente.

Ocultando senhas

O Readline padrão não possui uma API de alto nível universal para senha mascarada. É possível controlar terminal e escrita, mas implementações manuais são fáceis de quebrar em plataformas diferentes. Prefira uma biblioteca madura para prompts secretos ou leia o valor por arquivo/secret manager.

Nunca exiba, registre ou mantenha a senha no histórico. Limpe Buffers temporários quando possível; veja Buffer no Node.js.

Lendo arquivo linha por linha

const fs = require('node:fs');
const readline = require('node:readline');

async function processFile(filename) {
  const stream = fs.createReadStream(filename, {
    encoding: 'utf8'
  });

  const rl = readline.createInterface({
    input: stream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    await processLine(line);
  }
}

crlfDelay: Infinity trata CRLF como um único fim de linha. O arquivo é processado progressivamente, sem carregar tudo na memória. Consulte File System no Node.js.

Backpressure ao processar linhas

O loop for await aguarda processLine() antes de buscar a próxima linha, criando processamento sequencial. Isso é adequado quando a ordem importa ou a dependência possui limite baixo.

Para paralelismo, use uma fila com concorrência limitada. Não dispare Promises ilimitadas para milhões de linhas.

const pending = new Set();
const concurrency = 5;

for await (const line of rl) {
  const task = processLine(line).finally(() => {
    pending.delete(task);
  });

  pending.add(task);

  if (pending.size >= concurrency) {
    await Promise.race(pending);
  }
}

await Promise.all(pending);

Linhas muito grandes

Readline acumula bytes até encontrar um fim de linha. Um arquivo com uma única linha enorme pode consumir muita memória. Se a entrada não é confiável, aplique limite de tamanho antes ou use um parser que controle chunks e comprimento.

Arquivos CSV

Não processe CSV apenas dividindo cada linha por vírgula. Campos podem conter vírgulas e quebras de linha entre aspas. Use um parser CSV que trabalhe por stream e siga a especificação do formato.

Comandos e processos externos

Uma CLI pode iniciar ferramentas externas, mas nunca concatene a linha digitada em uma string de shell. Mapeie comandos permitidos e use argumentos separados com spawn(). Veja Child Process no Node.js.

const handlers = {
  status: showStatus,
  sync: runSync,
  exit: () => rl.close()
};

const handler = handlers[command];
if (!handler) {
  console.log('Comando desconhecido');
} else {
  await handler();
}

Saída e cursor

O módulo oferece métodos como clearLine(), clearScreenDown(), cursorTo() e moveCursor() para atualizar o terminal:

readline.cursorTo(process.stdout, 0);
readline.clearLine(process.stdout, 0);
process.stdout.write('Progresso: 50%');

Use apenas quando stdout é TTY. Em logs redirecionados, sequências de controle podem poluir a saída.

AbortSignal em question()

A API de Promises pode aceitar um sinal conforme a versão:

const controller = new AbortController();

setTimeout(() => controller.abort(), 30_000);

const answer = await rl.question('Resposta: ', {
  signal: controller.signal
});

Confirme o suporte na versão mínima. Consulte AbortController no Node.js.

Tratamento de erros

Streams de entrada podem emitir erro. Registre listeners quando a origem é arquivo ou socket:

stream.on('error', error => {
  logger.error({ error }, 'Falha ao ler entrada');
});

Em CLI, retorne um código de saída diferente de zero para falhas. Não exponha stack trace por padrão ao usuário final; permita um modo verbose.

Separando interface e lógica

Não coloque toda a regra dentro de listeners. Crie funções que recebem valores e retornam resultados. A interface coleta entrada e exibe saída:

function createUser(input, dependencies) {
  validateUser(input);
  return dependencies.repository.create(input);
}

Essa separação simplifica testes e permite reutilizar a lógica em API ou worker.

Como testar uma CLI

Injete streams:

const { PassThrough } = require('node:stream');

const input = new PassThrough();
const output = new PassThrough();

const rl = readline.createInterface({ input, output });

input.write('Ana\n');

Capture a saída e confirme prompts e resultados. Para lógica principal, teste funções sem terminal. O guia de Node Test Runner mostra mocks e recursos temporários.

Observabilidade

Não envie cada tecla ou resposta para telemetria. Registre nome do comando, duração e resultado, removendo argumentos sensíveis. Para processamento de arquivo, acompanhe linhas processadas, erros e taxa, sem registrar conteúdo completo.

Erros comuns

  • Esquecer rl.close(): o processo permanece aberto.
  • Não validar respostas: strings inválidas chegam à lógica.
  • Exigir modo interativo no CI: o comando trava aguardando entrada.
  • Guardar senha no histórico: o segredo fica exposto.
  • Carregar arquivo inteiro: memória cresce desnecessariamente.
  • Executar linha como shell: permite injeção de comandos.
  • Paralelizar sem limite: milhões de Promises são criadas.
  • Usar controle de cursor sem TTY: logs recebem caracteres especiais.

Boas práticas para produção

  • Prefira a API de Promises em fluxos sequenciais.
  • Feche a interface em finally.
  • Valide e converta cada resposta.
  • Ofereça flags para execução não interativa.
  • Não armazene segredos no histórico.
  • Use for await para arquivos grandes.
  • Limite concorrência e tamanho de linha.
  • Mapeie comandos permitidos.
  • Trate SIGINT e erros de stream.
  • Separe interface da lógica de negócio.

Conclusão

O Readline no Node.js permite criar prompts, shells simples e processadores de texto usando streams nativas. A API de Promises torna perguntas sequenciais fáceis de organizar, enquanto o iterador assíncrono processa arquivos linha por linha.

Uma CLI confiável precisa funcionar em terminais e automações, validar entradas, proteger segredos e fechar recursos. Com lógica separada e limites de concorrência, Readline atende desde perguntas simples até ferramentas administrativas e importadores de grandes arquivos.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita