Skip to main content

In this article

10 recursos modernos do runtime do Node.js para começar a usar em 2024

Escrito por
feature java dto

29 de maio de 2024

0 minutos de leitura
10 Node.js runtime features you SHOULD be using in 2024

O cenário dos runtimes JavaScript no lado do servidor está repleto de inovações: o Bun vem avançando com APIs compatíveis com Node.js, enquanto o runtime do Node.js oferece uma biblioteca padrão abrangente e diversos recursos.

Com a chegada de 2024, este artigo é uma ótima oportunidade para ficar por dentro dos recursos e funcionalidades mais recentes do runtime do Node.js. Manter-se atualizado não é apenas “acompanhar os tempos”: é aproveitar o poder das APIs modernas para escrever código mais eficiente, com melhor desempenho e mais seguro.

Neste artigo, vamos explorar 10 recursos modernos do runtime do Node.js que todo desenvolvedor deveria começar a usar em 2024. De APIs recém-lançadas aos recursos interessantes dos novatos Bun e Deno, você vai conhecer tudo isso.

Pré-requisito: versão LTS do Node.js

Antes de explorar esses recursos modernos, verifique se você está usando a versão LTS (suporte de longo prazo) do Node.js. No momento da redação deste artigo, a versão LTS mais recente do Node.js é a v20.14.0.

Para verificar sua versão do Node.js, use o comando:

node --version

Se você não estiver usando a versão LTS, considere usar um gerenciador de versões, como fnm ou nvm, para alternar facilmente entre diferentes versões do Node.js.

O que há de novo no Node.js 20?

Nas próximas seções, vamos abordar alguns recursos novos introduzidos nas versões recentes do Node.js. Alguns são estáveis, outros ainda são experimentais e alguns já eram compatíveis há algum tempo, mas talvez você ainda não os conheça.

Vamos abordar os seguintes tópicos:

  1. Test runner do Node.js

  2. Mocking nativo do Node.js

  3. Cobertura de testes nativa do Node.js

  4. Modo watch do Node.js

  5. Corepack do Node.js

  6. Carregador de .env do Node.js

  7. import.meta.file do Node.js para __dirname e __file

  8. Promises de timers nativas do Node.js

  9. Módulo de permissões do Node.js

  10. Módulo de políticas do Node.js

1. O test runner nativo do Node.js

O que usávamos antes de o Node.js introduzir um test runner no runtime nativo? Até então, você provavelmente recorria a uma das opções populares, como node-tap, jest, mocha ou vitest. 

Vamos aprender a aproveitar o test runner nativo do Node.js no seu fluxo de desenvolvimento. Para começar, você precisa importar o módulo de testes do Node.js para o arquivo de teste, como mostrado abaixo:

import { test } from 'node:test';

Agora, vamos ver as diferentes etapas para usar o test runner do Node.js.

Executar um único teste com node:test

Para criar um único teste, use a função test, passando o nome do teste e uma função de callback. É nessa função de callback que você define a lógica do teste.

import { test } from "node:test";
import assert from "node:assert";
import { add } from "../src/math.js";

test("should add two numbers", () => {
  const result = add(1, 2);
  assert.strictEqual(result, 3);
});

test("should fail to add strings", () => {
  assert.throws(() => {
    add("1", "2");
  });
});

Para executar esse teste, use o comando node --test, seguido do nome do arquivo de teste:

node --test tests/math.test.js

O test runner do Node.js consegue detectar e executar automaticamente os arquivos de teste do seu projeto. Por convenção, esses arquivos devem terminar com .test.js, mas essa convenção de nome não é obrigatória.

Se você omitir o argumento posicional do arquivo de teste, o test runner do Node.js usará algumas heurísticas e padrões glob para localizar os arquivos, como todos os arquivos nas pastas test/ ou tests/, além dos arquivos com o prefixo test- ou o sufixo .test.

Por exemplo, para localizar arquivos de teste com correspondência glob:

node --test '**/*.test.js'

Usar asserções de teste com node:assert

O test runner do Node.js oferece suporte a asserções por meio do módulo integrado assert. Você pode usar diferentes métodos, como assert.strictEqual, para verificar seus testes.

import assert from 'node:assert';

test('Test 1', () => {
  assert.strictEqual(1 + 1, 2);
});

Suítes de teste e hooks com o test runner nativo do Node.js

A função describe agrupa testes relacionados em uma suíte. Isso deixa seus testes mais organizados e fáceis de gerenciar.

import { test, describe } from "node:test";

describe('My Test Suite', () => {
  test('Test 1', () => {
    // Test 1 logic
  });

  test('Test 2', () => {
    // Test 2 logic
  });
});

Hooks de teste são funções especiais executadas antes ou depois dos testes. Eles são úteis para preparar ou limpar os ambientes de teste.

test.beforeEach(() => {
  // Runs before each test
});

test.afterEach(() => {
  // Runs after each test
});

Você também pode optar por ignorar um teste usando a função test.skip. Isso é útil quando você quer desconsiderar temporariamente um teste específico.

test.skip('My skipped test', () => {
  // Test logic
});

Além disso, o test runner do Node.js oferece diferentes reporters, que formatam e exibem os resultados dos testes de várias maneiras. Você pode especificar um reporter usando a opção --reporter.

node --test --test-reporter=tap

Está na hora de abandonar o Jest?

Embora o Jest seja um framework de testes popular na comunidade Node.js, ele tem algumas desvantagens que tornam o test runner nativo do Node.js uma opção mais atraente.

Ao instalar o Jest, mesmo que apenas como dependência de desenvolvimento, você adiciona 277 dependências transitivas com diversas licenças, incluindo MIT, Apache-2.0, CC-BY-4.0 e uma licença desconhecida. Você sabia disso?

Grafo de dependências transitivas do Jest.
  • O Jest modifica variáveis globais, o que pode causar comportamentos inesperados nos testes.

  • O operador instanceof nem sempre funciona como esperado no Jest.

  • O Jest adiciona muitas dependências ao seu projeto, dificultando manter dependências de terceiros atualizadas e obrigando você a gerenciar sem necessidade problemas de segurança e outras questões relacionadas a dependências usadas durante o desenvolvimento.

  • O Jest pode ser mais lento que o test runner nativo do Node.js por causa da sobrecarga que adiciona.

Outros recursos interessantes do test runner nativo do Node.js incluem a execução de subtestes e testes concorrentes. Os subtestes permitem que cada callback de test() receba um argumento context, que possibilita criar testes aninhados com context.test. Os testes concorrentes são ótimos se você souber trabalhar com eles e evitar condições de corrida. Basta passar um possível objeto concurrency: true como segundo argumento da suíte de testes describe().

O que é um executor de testes?

Um executor de testes é uma ferramenta de software que permite aos desenvolvedores gerenciar e executar testes automatizados no código. O executor de testes do Node.js é um framework projetado para funcionar perfeitamente com o Node.js, oferecendo um ambiente completo para escrever e executar testes nas suas aplicações Node.js.

2. Mocking nativo do Node.js

Mocking é uma estratégia usada por desenvolvedores para isolar código durante os testes. O runtime do Node.js introduziu recursos nativos de mocking, que os desenvolvedores precisam conhecer e saber usar com eficiência.

Você provavelmente já usou recursos de mocking de outros frameworks de teste, como jest.spyOn ou mockResolvedValueOncel do Jest. Eles são úteis quando você quer evitar a execução do código real nos testes, por exemplo, requisições HTTP ou APIs do sistema de arquivos, e substituir essas operações por stubs e mocks que podem ser inspecionados depois.

Diferentemente de outros recursos do runtime do Node.js, como watch e coverage, o mocking não é classificado como experimental. No entanto, ele pode passar por mais mudanças, pois é um recurso novo, introduzido apenas no Node.js 18.

Mocking nativo do Node.js com import { mock } from 'node:test'

Vamos ver como usar o recurso de mocking nativo do Node.js em um exemplo prático. O test runner e o recurso de mocking de módulos estão disponíveis como funcionalidades estáveis no Node.js 20 LTS.

Vamos trabalhar com um módulo utilitário, dotenv.js, que carrega variáveis de ambiente de um arquivo .env. Também vamos usar um arquivo de teste, dotenv.test.js, para testar o módulo dotenv.js.

Veja nosso módulo dotenv desenvolvido internamente:

// dotenv.js
import fs from "node:fs/promises";

export async function loadEnv(path = ".env") {
  const rawDataEnv = await fs.readFile(path, "utf8");
  const env = {};
  rawDataEnv.split("\n").forEach((line) => {
    const [key, value] = line.split("=");
    env[key] = value;
  });

  return env;
}

No arquivo dotenv.js, temos uma função assíncrona, loadEnv, que lê um arquivo usando o método fs.readFile e divide o conteúdo em pares de chave e valor. Como você pode ver, ela usa a API nativa do sistema de arquivos do Node.js, fs.

Agora, vamos ver como testar essa função usando o recurso nativo de mocking do Node.js.

// dotenv.test.js
import { describe, test, mock } from "node:test";
import assert from "node:assert";
import fs from "node:fs/promises";

import { loadEnv } from "../src/dotenv.js";

describe("dotenv test suite", () => {
  test("should load env file", async () => {
    const mockImplementation = async (path) => {
      return "PORT=3000\n";
    };
    const mockedReadFile = mock.method(fs, "readFile", mockImplementation);

    const env = await loadEnv(".env");

    assert.strictEqual(env.PORT, "3000");
    assert.strictEqual(mockedReadFile.mock.calls.length, 1);
  });
});

No arquivo de teste, importamos o método mock de node:test e o usamos para criar uma implementação simulada de fs.readFile. Nessa implementação, retornamos a string "PORT=3000\n", independentemente do caminho do arquivo informado.

Em seguida, chamamos a função loadEnv e, usando o módulo assert, verificamos duas coisas:

  1. O objeto retornado tem uma propriedade PORT com o valor "3000".

  2. O método fs.readFile foi chamado exatamente uma vez.

Com o recurso nativo de mocking do Node.js, conseguimos isolar com eficiência a função loadEnv do sistema de arquivos e testá-la separadamente. Os recursos de mocking do Node.js 20 também permitem simular timers.

O que é mocking?

Em testes de software, mocking é o processo de substituir as funcionalidades reais de módulos específicos por versões artificiais. O principal objetivo é isolar a unidade de código que está sendo testada de dependências externas, garantindo que o teste verifique apenas a funcionalidade dessa unidade, e não a das dependências. O mocking também permite simular diferentes cenários, como erros nas dependências, que podem ser difíceis de reproduzir de forma consistente em um ambiente real.

3. Cobertura de testes nativa do Node.js

O que é cobertura de testes?

A cobertura de testes é uma métrica usada em testes de software. Ela ajuda os desenvolvedores a entender em que medida o código-fonte de um aplicativo está sendo testado. Isso é essencial porque revela partes da base de código que não foram testadas, permitindo identificar possíveis pontos fracos no software.

Por que a cobertura de testes é importante? Ela ajuda a garantir a qualidade do software, reduzindo o número de bugs e evitando regressões. Além disso, revela a eficácia dos seus testes e ajuda você a criar uma aplicação mais robusta, confiável e segura.

Usar a cobertura de testes nativa do Node.js

A partir da versão 20, o runtime do Node.js inclui recursos nativos de cobertura de testes. É importante observar, porém, que a cobertura de testes nativa do Node.js está marcada como experimental. Isso significa que, embora esteja disponível para uso, ela pode mudar em versões futuras.

Para usar a cobertura de testes nativa do Node.js, você precisa usar a flag de linha de comando --experimental-test-coverage. Veja como adicionar uma entrada test:coverage ao campo de scripts do seu package.json para executar os testes do projeto:

{
  "scripts": {
    "test": "node --test ./tests",
    "test:coverage": "node --experimental-test-coverage --test ./tests"
  }
}

No exemplo acima, o script test:coverage usa a flag --experimental-test-coverage para gerar dados de cobertura durante a execução dos testes.

Depois de executar npm run test:coverage, você verá um resultado parecido com este:

ℹ tests 7
ℹ suites 4
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 1
ℹ todo 1
ℹ duration_ms 84.018917
ℹ start of coverage report
ℹ ---------------------------------------------------------------------
ℹ file                 | line % | branch % | funcs % | uncovered lines
ℹ ---------------------------------------------------------------------
ℹ src/dotenv.js        | 100.00 |   100.00 |  100.00 | 
ℹ src/math.js          | 100.00 |   100.00 |  100.00 | 
ℹ tests/dotenv.test.js | 100.00 |   100.00 |  100.00 | 
ℹ tests/math.test.js   |  94.64 |   100.00 |   91.67 | 24-26
ℹ ---------------------------------------------------------------------
ℹ all files            |  96.74 |   100.00 |   94.44 |
ℹ ---------------------------------------------------------------------
ℹ end of coverage report

Este relatório mostra a porcentagem de instruções, ramificações, funções e linhas cobertas pelos testes.

A cobertura de testes nativa do Node.js é uma ferramenta poderosa para ajudar você a melhorar a qualidade das aplicações Node.js. Embora ainda esteja marcada como experimental, ela pode oferecer informações valiosas sobre a cobertura dos seus testes e orientar seus esforços de teste. Ao entender e aproveitar esse recurso, você pode garantir que seu código seja robusto, confiável e seguro.

4. Modo watch do Node.js

O modo watch do Node.js é um recurso poderoso para desenvolvedores, que permite acompanhar em tempo real as alterações nos arquivos do Node.js e executar scripts novamente de forma automática.

Antes de conhecer os recursos nativos de watch do Node.js, vale destacar o nodemon, um utilitário popular que atendia a essa necessidade nas versões anteriores do Node.js. Nodemon é um utilitário de linha de comando (CLI) desenvolvido para reiniciar a aplicação Node.js sempre que uma alteração é detectada no diretório de arquivos.

npm install -g nodemon
nodemon

Esse recurso é especialmente útil durante o desenvolvimento. Ele economiza tempo e aumenta a produtividade ao eliminar a necessidade de reiniciar manualmente o programa sempre que um arquivo é alterado.

Snyk Advisor mostrando o status de integridade do pacote npm nodemon.

Com os avanços do próprio Node.js, o runtime agora oferece uma funcionalidade integrada para obter os mesmos resultados. Assim, você não precisa instalar dependências extras de terceiros, como nodemon, nos seus projetos.

Antes de começar o tutorial, é importante observar que o recurso nativo de modo watch do Node.js ainda é experimental e pode mudar. Sempre verifique se você está usando uma versão do Node.js compatível com esse recurso.

Usar os recursos nativos de watch do Node.js 20

O Node.js 20 introduz recursos nativos para observar arquivos usando a flag de linha de comando --watch. É simples de usar e pode até corresponder a padrões glob para atender a necessidades mais complexas de monitoramento de arquivos.

Para usar o comando --watch, adicione-o ao script do Node.js na linha de comando, como mostrado abaixo:

node --watch app.js

Para usar padrões glob, você pode combinar a flag --watch com um padrão específico para observar vários arquivos ou diretórios. Isso é especialmente útil quando você quer monitorar um grupo de arquivos que corresponde a um padrão específico:

node --watch 'lib/**/*.js' app.js

A flag --watch também pode ser usada com --test para executar os testes novamente sempre que os arquivos de teste forem alterados:

node --watch --test '**/*.test.js'

Essa combinação pode acelerar bastante seu processo de desenvolvimento orientado a testes (TDD), executando os testes automaticamente sempre que você fizer uma alteração.

É importante observar que, no Node.js 20, o recurso de modo watch ainda está marcado como experimental. Isso significa que, embora funcione plenamente, talvez não seja tão estável ou otimizado quanto outros recursos não experimentais.

Na prática, você pode encontrar algumas particularidades ou bugs ao usar a flag --watch.

5. Corepack do Node.js

O Corepack do Node.js é um recurso interessante que vale a pena explorar. Ele foi introduzido no Node.js 16 e ainda é considerado experimental. Isso torna ainda mais interessante conhecer o que ele oferece e como aproveitá-lo em seus projetos JavaScript.

O que é o Corepack?

O Corepack é um projeto sem dependências de runtime que funciona como uma ponte entre os projetos Node.js e os gerenciadores de pacotes que eles devem usar. Depois de instalado, ele disponibiliza um programa chamado corepack, que você pode usar nos projetos para garantir o uso do gerenciador de pacotes correto sem se preocupar com a instalação global.

Por que usar o Corepack?

Como desenvolvedores JavaScript, muitas vezes trabalhamos com vários projetos, cada um podendo ter seu próprio gerenciador de pacotes preferido. Você sabe como é: um projeto gerencia as dependências com pnpm e outro com yarn, então você acaba tendo que alternar entre diferentes versões dos gerenciadores de pacotes.

Isso pode gerar conflitos e inconsistências. O Corepack resolve esse problema permitindo que cada projeto especifique e use seu gerenciador de pacotes preferido de forma simples.

Além disso, o Corepack isola seu projeto do sistema global, garantindo que ele continue executável mesmo se pacotes globais forem atualizados ou removidos. Isso aumenta a consistência e a confiabilidade do projeto.

Como instalar e usar o Corepack

Instalar o Corepack é bem simples. Como ele vem incluído no Node.js a partir da versão 16, basta instalar ou atualizar o Node.js para essa versão ou uma mais recente.

Depois de instalado, você pode definir o gerenciador de pacotes do projeto no arquivo package.json, assim:

{
  "packageManager": "yarn@2.4.1"
}

Em seguida, você pode usar o Corepack no projeto desta forma:

corepack enable

Se você digitar yarn no diretório do projeto e não tiver o Yarn instalado, o Corepack vai detectar e instalar automaticamente a versão correta.

Assim, a versão 2.4.1 do Yarn será usada para instalar as dependências do projeto, independentemente da versão global do Yarn instalada no sistema.

Para instalar o Yarn globalmente ou usar uma versão específica, execute:

corepack install --global yarn@stable

Corepack: ainda é um recurso experimental

Apesar de ter sido introduzido no Node.js 16, o Corepack ainda é considerado experimental. Isso significa que, embora a expectativa seja de que funcione bem, ele continua em desenvolvimento ativo e alguns aspectos de seu comportamento podem mudar no futuro.

Ainda assim, o Corepack é fácil de instalar e simples de usar, além de adicionar uma camada extra de confiabilidade aos seus projetos. Sem dúvida, vale a pena explorar esse recurso e incorporá-lo ao seu fluxo de desenvolvimento.

6. Carregador de arquivos .env do Node.js

A configuração de aplicações é fundamental e, como desenvolvedor Node.js, tenho certeza de que você já precisou gerenciar credenciais de API, portas de servidor ou configurações de banco de dados.

Como desenvolvedores, precisamos de uma forma de definir configurações diferentes para cada ambiente sem alterar o código-fonte. Uma maneira popular de fazer isso em aplicações Node.js é usar variáveis de ambiente armazenadas em arquivos .env.

O pacote dotenv do npm

Antes de o Node.js oferecer suporte nativo ao carregamento de arquivos .env, os desenvolvedores usavam principalmente o pacote dotenv do npm. O pacote dotenv carrega as variáveis de ambiente de um arquivo .env para process.env, onde ficam disponíveis em toda a aplicação.

Veja um exemplo de uso do pacote dotenv:

require('dotenv').config();

console.log(process.env.MY_VARIABLE);

Essa solução funcionava bem, mas exigia adicionar outra dependência ao projeto. Com a introdução do carregador nativo de arquivos .env, agora você pode carregar as variáveis de ambiente diretamente, sem precisar de pacotes externos.

Suporte nativo do Node.js ao carregamento de arquivos .env

A partir do Node.js 20, o runtime inclui um recurso integrado para carregar variáveis de ambiente de arquivos .env. Esse recurso ainda está em desenvolvimento ativo, mas já transformou a rotina dos desenvolvedores.

Para carregar um arquivo .env, podemos usar a flag de CLI --env-file ao iniciar a aplicação Node.js. Essa flag especifica o caminho do arquivo .env a ser carregado.

node --env-file=./.env index.js

Isso carrega as variáveis de ambiente do arquivo .env especificado para process.env. Depois, elas ficam disponíveis na aplicação como antes.

Como carregar vários arquivos .env

O carregador de arquivos .env do Node.js também permite carregar vários arquivos .env. Isso é útil quando você tem conjuntos diferentes de variáveis de ambiente para cada ambiente, como desenvolvimento, testes e produção.

Você pode especificar várias flags --env-file para carregar vários arquivos. Eles são carregados na ordem especificada, e as variáveis dos arquivos posteriores substituem as dos anteriores.

Veja um exemplo:

node --env-file=./.env.default --env-file=./.env.development index.js

Neste exemplo, ./.env.default contém as variáveis padrão, e ./.env.development contém as variáveis específicas de desenvolvimento. As variáveis de ./.env.development que também estiverem em ./.env.default substituirão as variáveis correspondentes em ./.env.default.

O suporte nativo do Node.js ao carregamento de arquivos .env é uma grande melhoria para quem desenvolve com Node.js. Ele simplifica o gerenciamento de configurações e elimina a necessidade de um pacote adicional. Comece a usar a flag de CLI --env-file em suas aplicações Node.js e aproveite essa praticidade na prática.

7. Suporte do Node.js a import.meta para __dirname e __file

Se você está migrando das convenções de módulos CommonJS do Node.js, já conhece filename e __dirname, usados para obter o nome do diretório e o caminho do arquivo atual. Até pouco tempo atrás, porém, esses valores não estavam facilmente disponíveis no ESM, e era preciso escrever o código a seguir para obter __dirname:

import url from 'url'
import path from 'path'
const dirname = path.dirname(url.fileURLToPath(import.meta.url))

Ou, se você é fã de Matteo Collina, talvez tenha optado por usar o pacote npm desm dele.

Matteo Collina, criador do pacote npm desm, que disponibiliza __dirname e __filename em projetos ESM por meio dos dados do objeto import.meta.

O Node.js evolui continuamente para oferecer aos desenvolvedores maneiras mais eficientes de lidar com arquivos e caminhos. Uma mudança importante para quem desenvolve com Node.js chegou nas versões v20.11.0 e v21.2.0: o suporte integrado a import.meta.dirname e import.meta.filename.

Como usar import.meta.filename e import.meta.dirname no Node.js

Felizmente, com a introdução de import.meta.filename e import.meta.dirname, esse processo ficou muito mais simples. Veja um exemplo de como carregar um arquivo de configuração usando esses novos recursos.

Imagine que você precisa carregar um arquivo de configuração YAML localizado no mesmo diretório do arquivo JavaScript. Veja como fazer isso:

import fs from 'fs';

const { dirname: __dirname, filename: __filename } = import.meta;
const projectSetup = fs.readFileSync(`${__dirname}/setup.yml`, "utf8");

console.log(projectSetup);

Neste exemplo, usamos import.meta.dirname para obter o nome do diretório do arquivo atual e atribuí-lo à variável __dirname, por conveniência e para manter a convenção de código do CommonJS.

8. Promises nativas para timers no Node.js

O Node.js, um runtime JavaScript popular baseado no mecanismo V8 do Chrome, sempre buscou facilitar a vida dos desenvolvedores com atualizações constantes e novos recursos.

Embora o Node.js ofereça suporte ao uso nativo de timers com sintaxe de promises desde a versão v15, admito que não tenho usado esse recurso com frequência.

Timers setTimeout() e setInterval() do JavaScript: uma breve recapitulação

Antes de falar sobre promises nativas para timers, vamos relembrar brevemente os timers setTimeout() e setInterval() do JavaScript.

A API setTimeout() é uma função JavaScript que executa uma função ou um trecho de código especificado quando o timer termina.

setTimeout(function(){ 
    console.log("Hello World!"); 
}, 3000);

No código acima, "Hello World!" será exibido no console após 3 segundos (3000 milissegundos).

Já setInterval() executa repetidamente a função especificada, com um intervalo entre cada chamada.

setInterval(function(){ 
    console.log("Hello again!"); 
}, 2000);

No código acima, "Hello again!" será exibido no console a cada 2 segundos (2000 milissegundos).

O jeito antigo: envolver setTimeout() em uma promise

Antigamente, os desenvolvedores precisavam envolver artificialmente a função setTimeout() em uma promise para usá-la de forma assíncrona. Isso permitia usar setTimeout() com async/await.

Veja como isso era feito:

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function demo() {
  console.log('Taking a break...');
  await sleep(2000);
  console.log('Two seconds later...');
}

demo();

O código exibiria "Taking a break...", aguardaria dois segundos e, em seguida, exibiria "Two seconds later...".

Embora funcionasse, essa abordagem adicionava complexidade desnecessária ao código.

Promises nativas para timers no Node.js: um jeito mais simples

Com as promises nativas para timers do Node.js, não precisamos mais envolver setTimeout() em uma promise. Em vez disso, podemos usar setTimeout() diretamente com async/await. Assim, o código fica mais limpo, legível e fácil de manter. Veja como usar as promises nativas para timers do Node.js:

const {
  setTimeout,
} = require('node:timers/promises');

setTimeout(2000, 'Two seconds later...').then((res) => {
  console.log(res);  
});

console.log('Taking a break...');

No código acima, setTimeout() é importado de node:timers/promises. Depois, usamos a função diretamente com async/await. O código exibirá "Taking a break...", aguardará dois segundos e, em seguida, exibirá "Two seconds later...".

Isso simplifica bastante a programação assíncrona e facilita a leitura, a escrita e a manutenção do código.

9. Modelo de permissões do Node.js

Rafael Gonzaga, que hoje faz parte do TSC do Node.js, retomou o trabalho no módulo de permissões do Node.js. Assim como no Deno, ele oferece um conjunto de restrições configuráveis de recursos no nível do processo.

Diante das preocupações com a segurança da cadeia de suprimentos, dos pacotes npm maliciosos e de outros riscos, é cada vez mais importante gerenciar e controlar os recursos aos quais suas aplicações Node.js têm acesso, tanto por segurança quanto por conformidade.

Nesse contexto, o Node.js introduziu um recurso experimental chamado módulo de permissões, usado para gerenciar as permissões de recursos nas aplicações Node.js. Para ativá-lo, use a flag de linha de comando --experimental-permission.

Modelo de permissões de recursos do Node.js

O modelo de permissões do Node.js oferece uma abstração para gerenciar o acesso a vários recursos, como sistemas de arquivos, redes, variáveis de ambiente e threads de worker. Esse recurso é especialmente útil quando você quer limitar os recursos aos quais determinada parte da aplicação pode ter acesso.

Entre as restrições de recursos que você pode definir com o modelo de permissões estão:

  • Leitura e gravação no sistema de arquivos com --allow-fs-read=* e --allow-fs-write=*. Você também pode especificar diretórios e caminhos de arquivos específicos, além de incluir vários recursos repetindo as flags.

  • Execução de processos filhos com --allow-child-process

  • Execução de threads de worker com --allow-worker

O modelo de permissões do Node.js também disponibiliza uma API de runtime, process.permission.has(resource, value), para consultar permissões de acesso específicas.

Se você tentar acessar um recurso sem permissão — por exemplo, ler o arquivo .env —, verá um erro ERR_ACCESS_DENIED:

> start:protected
> node --env-file=.env --experimental-permission server.js

node:internal/modules/cjs/loader:197
  const result = internalModuleStat(filename);
                 ^

Error: Access to this API has been restricted
    at stat (node:internal/modules/cjs/loader:197:18)
    at Module._findPath (node:internal/modules/cjs/loader:682:16)
    at resolveMainPath (node:internal/modules/run_main:28:23)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:24)
    at node:internal/main/run_main_module:28:49 {
  code: 'ERR_ACCESS_DENIED',
  permission: 'FileSystemRead',
  resource: '/Users/lirantal/repos/modern-nodejs-runtime-features-2024/server.js'
}

Node.js v21.6.1

Exemplo do modelo de permissões do Node.js

Imagine uma aplicação Node.js que processa uploads de arquivos. Você quer restringir essa parte da aplicação para que ela tenha acesso somente a um diretório específico, onde os arquivos enviados são armazenados.

Ative o recurso experimental de permissões ao iniciar a aplicação Node.js com a flag --experimental-permission.

node --experimental-permission ./app.js

Também queremos permitir especificamente que a aplicação leia dois arquivos confiáveis, .env e setup.yml. Para isso, precisamos atualizar o código acima:

node --experimental-permission --allow-fs-write=/tmp/uploads --allow-fs-read=.env --allow-fs-read=setup.yml ./app.js

Assim, se a aplicação tentar gravar em recursos do sistema baseados em arquivos fora do caminho de upload especificado, a execução será interrompida com um erro.

Veja no exemplo de código a seguir como envolver o acesso a um recurso em try/catch e também usar a API de runtime de permissões do Node.js como outra forma de garantir o acesso sem gerar uma exceção de erro:

 const { dirname: __dirname, filename: __filename } = import.meta;
// @TODO to avoid the Node.js resource permission issue you should update
// the path to be `setup.yml` in the current directory and not `../setup.yml`.
// the outside path for setup.yml was only changed in the source code to
// show you how Node.js resource permission module will halt if trying to access
// something outside the current directory.
const filePath = `${__dirname}/../setup.yml`;
try {
  const projectSetup = fs.readFileSync(filePath, "utf8");
  // @TODO do something with projectSetup if you want to
} catch (error) {
  console.error(error.code);
}
// @TODO or consider using the permissions runtime API check:
if (!process.permission.has("read", filePath)) {
  console.error("no permissions to read file at", filePath);
}

É importante lembrar que o recurso de permissões do Node.js ainda é experimental e pode sofrer alterações.

Sobre permissões e práticas de segurança para ambientes de produção, confira estas publicações da Snyk para saber mais sobre como criar aplicações Node.js seguras:

Essas publicações oferecem um guia completo para criar imagens de contêiner seguras para aplicações web Node.js, algo essencial para desenvolver aplicações Node.js seguras.

10. Módulo de políticas do Node.js

O módulo de políticas do Node.js é um recurso de segurança criado para impedir que códigos maliciosos sejam carregados e executados em uma aplicação Node.js. Embora não rastreie a origem do código carregado, ele oferece uma sólida defesa contra possíveis ameaças.

O módulo de políticas usa a opção de CLI --experimental-policy para ativar o carregamento de código baseado em políticas. Essa opção recebe como argumento um arquivo de manifesto de políticas (em formato JSON). Por exemplo, --experimental-policy=policy.json.

O arquivo de manifesto de políticas contém as regras que o Node.js segue ao carregar módulos. Isso oferece uma maneira robusta de controlar que tipo de código é carregado na sua aplicação.

Como implementar o módulo de políticas do Node.js: guia passo a passo

Vamos ver um exemplo simples de como usar o módulo de políticas do Node.js:

1. Crie um arquivo de políticas. Ele deve ser um arquivo JSON que especifica as políticas da sua aplicação para o carregamento de módulos. Vamos chamá-lo de policy.json. 

Por exemplo:

    {
      "resources": {
        "./moduleA.js": {
          "integrity": "sha384-xxxxx"
        },
        "./moduleB.js": {
          "integrity": "sha384-yyyyy"
        }
      }
    }

Esse arquivo de políticas especifica que moduleA.js e moduleB.js precisam ter valores de integridade específicos para serem carregados.

No entanto, não é simples gerar o arquivo de políticas para todas as dependências diretas e transitivas. Há alguns anos, Bradley Meck criou o pacote npm node-policy, que oferece uma CLI para automatizar a geração desse arquivo.

2. Execute sua aplicação Node.js com a opção --experimental-policy :

  node --experimental-policy=policy.json app.js

Esse comando instrui o Node.js a seguir as políticas especificadas em policy.json ao carregar módulos em app.js.

3. Para proteger o arquivo de políticas contra adulteração, você pode fornecer um valor de integridade para o próprio arquivo usando a opção --policy-integrity :

    node --experimental-policy=policy.json --policy-integrity="sha384-zzzzz" app.js

Esse comando garante que a integridade do arquivo de políticas seja preservada, mesmo que ele seja alterado no disco.

Limitações da política de integridade do Node.js 

O runtime do Node.js não tem recursos integrados para gerar ou gerenciar o arquivo de políticas. Isso pode trazer dificuldades, como gerenciar políticas diferentes para ambientes de produção e desenvolvimento, além de lidar com importações dinâmicas de módulos.

Outra limitação é que, se você já tiver um pacote npm malicioso no estado atual, será tarde demais para gerar um arquivo de políticas de integridade dos módulos.

Recomendo acompanhar as atualizações nessa área e adotar esse recurso gradualmente.

Para saber mais sobre o módulo de políticas do Node.js, confira o artigo como introduzir políticas experimentais de integridade no Node.js, que traz um tutorial passo a passo mais detalhado sobre como trabalhar com a integridade das políticas do Node.js.

Conclusão

Ao conhecer os recursos modernos do runtime do Node.js que você deve começar a usar em 2024, fica claro que eles foram criados para simplificar seu processo de desenvolvimento, melhorar o desempenho das aplicações e reforçar a segurança. Não são apenas tendências: esses recursos têm grande potencial para transformar a maneira como desenvolvemos com Node.js.

Reforce a segurança do Node.js com a Snyk

Embora esses recursos do Node.js possam melhorar significativamente seu processo de desenvolvimento e o desempenho das aplicações, é essencial ficar atento a possíveis ameaças à segurança. A Snyk pode ajudar você nessa tarefa. Essa poderosa ferramenta ajuda a encontrar e corrigir vulnerabilidades conhecidas nas dependências do Node.js e a manter um ecossistema de desenvolvimento seguro.

Para aproveitar o que a Snyk oferece, cadastre-se gratuitamente aqui e comece sua jornada rumo a um desenvolvimento Node.js mais seguro.

Publicado em: