Ler arquivo(s) texto com Express Node.js
O objetivo deste artigo é mostrar como criar um endpoint, GET /arquivos
, que retorna o conteúdo de um ou mais arquivos .txt
. O framework utilizado será o Express que roda com o Node.js.
Antes de entrar no mundo do Express, se olharmos a documentação oficial do Node.js sobre como ler um arquivo, vamos encontrar a função readFile
e um exemplo bem simples:
Veja que o readFile
trabalha com um callback, que é o segundo parâmetro que passamos para a função. Então quando o readFile
finaliza a leitura do arquivo, este callback é executado e ali conseguimos pegar o conteúdo do arquivo ou o erro durante a leitura do mesmo.
Criando o projeto Express
Antes de entrarmos na implementação da leitura dos arquivos texto, vamos estruturar um projeto Express.
Para criar e configurar um projeto em Express basta rodar os comandos abaixo:
mkdir ler-arquivo-texto-express-nodejs
npm install express
Crie um arquivo chamado index.js
com o seguinte conteúdo:
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/arquivos', (req, res) => {
res.json({ ok: 'Estou funcionando' });
});
app.listen(3000);
Iniciando a aplicação acima através do comando node index.js
podemos obter a resposta { ok: 'Estou funcionando' }
ao efetuar um GET http://localhost:3000/arquivos
.
Como ler um arquivo texto
Para efeito de teste eu criei dois arquivos no mesmo diretório do index.js
:
arquivo1.txt
arquivo2.txt
Como primeiro objetivo vamos efetuar a leitura do arquivo1.txt
utilizando a função readFile
:
const { readFile } = require('fs');
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/arquivos', (req, res) => {
console.log('1');
const caminhoArquivo = './arquivo1.txt';
console.log('2');
readFile(caminhoArquivo, (err, data) /* callback */ => {
console.log('4');
if (err) {
res.status(500).send(err);
return;
}
res
.set({ 'Content-Type': 'text/plain' })
.send(data);
});
console.log('3');
}); /* Final do GET /arquivos */
app.listen(3000);
Deixei alguns pontos de console.log
para mostrar como será a ordem de execução, visto que o readFile
faz a leitura do arquivo de forma assíncrona. Então quando efetuarmos uma requisição GET /arquivos
, o Node.js irá executar o código da seguinte forma:
- Atribuir o valor para a
const caminhoArquivo
- Chamar a função
readFile
- Chegará no
/* Final do GET /arquivos */
- Depois de alguns milisegundos o
callback
doreadFile
será chamado e se tudo deu certo o conteúdo do arquivo será entregue como resposta da requisição.
Veja que as marcações dos console.log
batem exatamente com a explicação acima:
Resultado:
Como ler vários arquivos texto com o readFile
Até este momento ler um arquivo txt e entregar seu conteúdo ficou bem simples com a função readFile
. Vamos complicar um pouco mais as coisas e retornar o conteúdo concatenado do arquivo1.txt
e arquivo2.txt
, mas sempre mantenha em mente que o readFile
é assíncrono.
Para facilitar o trabalho vamos isolar a leitura do arquivo em um função chamada lerArquivo
e trabalhar com Promise
. Logo a seguir vou explicar melhor os detalhes:
JavaScript é uma linguagem síncrona mas nos provê alguns mecânismos para ter um comportamento assíncrono. Uma das formas é através dos callbacks, onde passamos uma função como parâmetro de um método e a mesma é executada em um dado instante sem "travar" a execução do seu código e consequentemente sem travar a interface.
Conforme o código cresce, vamos ter um monte de callbacks e isto vira uma bagunça e daí vêm o termo "callback hell". Com a evolução surgiu aPromise
que ajudou a organizar esses callbacks. Depois surgiu oasync/await
que é uma extensão das Promises para simplificar sua utilização.
Fonte: https://pt.stackoverflow.com/questions/413563/async-await-x-sync - trecho de uma resposta que escrevi no stackoverflow
function lerArquivo(caminhoArquivo) {
console.log(caminhoArquivo, 1);
return new Promise((resolve, reject) => {
console.log(caminhoArquivo, 3);
readFile(caminhoArquivo, (err, data) /* callback */ => {
console.log(caminhoArquivo, 4, err ? 'Erro' : 'Sucesso');
err ? reject(err) : resolve(data);
});
});
console.log(caminhoArquivo, 2);
}
A primeira coisa que fizemos foi isolar a leitura do arquivo em uma função chamada lerArquivo
que recebe como único parâmetro de entrada o caminho do arquivo que deve ser lido.
A segunda coisa é que esta função retorna uma "promessa", em JavaScript Promise
, que indica que haverá um trabalho assíncrono. Quando esta "promessa" finaliza seu trabalho com sucesso ela chama a função resolve
com o resultado do processamento ou a função reject
com os detalhes do erro. Por esta razão quando o readFile
retornar um erro na variável err
nós executamos o reject
ao invés do resolve
.
Olhando as marcações dos console.log
acima, a ordem de execução é: 1, 2, 3, 4
Agora vamos juntar tudo e novamente as explicações estão logo a seguir:
const { readFile } = require('fs');
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/arquivos', (req, res) => {
const caminhoArquivos = [
'./arquivo1.txt',
'./arquivo2.txt'
];
const promises = [];
for (const caminhoArquivo of caminhoArquivos) {
promises.push(lerArquivo(caminhoArquivo));
}
Promise.all(promises).then(conteudoArquivos => {
res
.set({ 'Content-Type': 'text/plain' })
.send(conteudoArquivos.join('\n'));
}).catch(err => {
res.status(500).send(err);
});
}); /* Final do GET /arquivos */
function lerArquivo(caminhoArquivo) {
console.log(caminhoArquivo, 1);
return new Promise((resolve, reject) => {
console.log(caminhoArquivo, 3);
readFile(caminhoArquivo, (err, data) /* callback */ => {
console.log(caminhoArquivo, 4, err ? 'Erro' : 'Sucesso');
err ? reject(err) : resolve(data);
});
});
console.log(caminhoArquivo, 2);
}
app.listen(3000);
Dentro da rota app.get('/arquivos', ...)
, criamos um array de string com o caminho de cada arquivo que deve ser lido.
Na sequência executamos um for
em cima deste array chamando a função lerArquivo
. O retorno desta função é uma Promise
que é salva em um outro array chamado promises
.
Após o for
devemos esperar todas as Promise
serem concluídas, lembre-se de que elas tem comportamento assíncrono, e para isto utilizamos o Promise.all
. Basicamente o Promise.all
espera que todas as Promise
passadas como parâmetro sejam finalizadas. Quando isto ocorre ele executa o .then
no caso de sucesso ou o .catch
caso haja algum erro.
O .then
retorna um array onde cada posição corresponde ao conteúdo de um dos arquivos. Então para entregá-lo ao cliente concatenamos este array com o .join()
.
Veja abaixo os prints dos console.log
e na sequência o resultado da requisição:
Considerações
Ler o arquivo é razoavelmente simples, basta entender como utilizar o callback do readFile
. O cenário fica mais complexo quando temos que ler vários arquivos, mas nada que não possa ser resolvido utilizando Promise
.
Um bom exemplo da vida real é quando temos restrições de atualização em um frontend, por exemplo, suponha que no frontend possamos adicionar em um único momento uma referência de script: <script src="minha-api/meus-scripts"></script>
. Veja que o navegador irá buscar em minha-api/meus-scripts
o conteúdo, e neste momento nossa API pode ler um ou mais arquivos e entregar o resultado concatenado.
Código completo:
const { readFile } = require('fs');
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/arquivos', (req, res) => {
const caminhoArquivos = [
'./arquivo1.txt',
'./arquivo2.txt'
];
const promises = [];
for (const caminhoArquivo of caminhoArquivos) {
promises.push(lerArquivo(caminhoArquivo));
}
Promise.all(promises).then(conteudoArquivos => {
res
.set({ 'Content-Type': 'text/plain' })
.send(conteudoArquivos.join('\n'));
}).catch(err => {
res.status(500).send(err);
});
}); /* Final do GET /arquivos */
function lerArquivo(caminhoArquivo) {
return new Promise((resolve, reject) => {
readFile(caminhoArquivo, (err, data) /* callback */ => {
err ? reject(err) : resolve(data);
});
});
}
app.listen(3000);
Links interessantes: