Skip to main content

Exemplos essenciais de back-end em Node.js para desenvolvedores em 2024

Escrito por

12 de junho de 2024

0 minutos de leitura

Em 2024, o desenvolvimento de back-end com Node.js continua se destacando como um ambiente de execução poderoso e flexível para criar aplicações escaláveis e eficientes, ainda mais com o surgimento de outros ambientes, como o Bun.

Neste artigo, quero apresentar uma introdução prática a exemplos essenciais de back-end em Node.js que demonstram o uso eficiente de recursos avançados do JavaScript e do Node.js. De aproveitar o padrão WHATWG Streams e a API Web Streams para processar dados com eficiência a usar o módulo crypto integrado do Node.js para segurança, trabalhar com Buffers para manipular dados binários, aproveitar Symbols para encapsulamento e definição de namespaces e usar template literals e tagged templates para gerar HTML e consultas SQL — cada seção traz trechos de código práticos, aplicáveis a situações reais, e insights.

Além de mostrar a versatilidade e a robustez do Node.js para resolver desafios de back-end, estes exemplos são uma referência valiosa para desenvolvedores que querem aprimorar suas soluções de back-end em 2024.

Capítulos deste artigo:

  1. O padrão WHATWG Streams, a API Web Streams e iteráveis assíncronos

  2. Como usar o módulo Crypto para validar assinaturas de webhooks

  3. Como trabalhar com buffers e dados binários brutos no Node.js

  4. Como usar Symbol no JavaScript para encapsulamento

  5. Template literals e tagged templates para gerar HTML e consultas SQL

Você pode consultar a qualquer momento os exemplos completos de back-end em Node.js com o código-fonte em este repositório do GitHub.

1. O padrão WHATWG Streams, a API Web Streams e iteráveis assíncronos

Em 2024, o uso de streams aumentou significativamente, especialmente no trabalho com modelos de linguagem grandes para inteligência artificial generativa (GenAI). O SDK da OpenAI para conclusão de chat é um ótimo exemplo dessa tendência. Um exemplo típico de transmissão de dados pela API do SDK da OpenAI seria assim:

const completion = await openai.chat.completions.create(config);

for await (const chunk of completion) {
  console.log(chunk);
}

Aqui, usamos o conceito de iteráveis assíncronos, que simplificou os fluxos de trabalho assíncronos no Node.js para acompanhar o estilo de programação baseado em promises. A instrução `for await…` cria um loop que percorre objetos iteráveis assíncronos e iteráveis síncronos, incluindo String, Array, objetos semelhantes a Array (por exemplo, arguments ou NodeList), TypedArray, Map, Set e iteráveis assíncronos ou síncronos definidos pelo usuário.

Conheça o padrão WHATWG Web Streams no Node.js

O Web Streams, parte do padrão WHATWG Streams, foi integrado ao Node.js e oferece uma maneira robusta de lidar com dados transmitidos em fluxo. Esse padrão permite que desenvolvedores leiam, gravem e transformem dados em fluxo com eficiência usando JavaScript.

Vamos ver um exemplo completo de como usar Web Streams no Node.js.

No exemplo a seguir, criamos uma função `createReadableStreamFromFile()` que usa a classe `ReadableStream` da API Web Streams para criar um fluxo de dados a partir de um arquivo. Em seguida, uma segunda função, `consumeStreamWithAsyncIterator()`, consome esse fluxo usando um iterador assíncrono.

import { ReadableStream } from "node:stream/web";
import fs from "fs";

function createReadableStreamFromFile(filePath) {
  const stream = new ReadableStream({
    start(controller) {
      const reader = fs.createReadStream(filePath);

      reader.on("data", (chunk) => {
        controller.enqueue(chunk);
        if (reader.readableFlowing === false) {
          reader.resume();
        }
      });

      reader.on("end", () => {
        controller.close();
      });

      reader.on("error", (err) => {
        controller.error(err);
      });
    },
  });

  return stream;
}

async function consumeStreamWithAsyncIterator(stream) {
  try {
    for await (const chunk of stream) {
      process.stdout.write(chunk);
    }
  } catch (err) {
    console.error("Error occurred while reading the stream:", err);
  }
}

const filePath = process.argv[2];
const stream = createReadableStreamFromFile(filePath);
consumeStreamWithAsyncIterator(stream);

Neste código, usamos a API Web Streams para ler dados de um arquivo em fluxo. A função `createReadableStreamFromFile()` retorna um objeto `ReadableStream` a partir do caminho de um arquivo. Em seguida, `consumeStreamWithAsyncIterator()` consome esse fluxo, lendo os dados em partes e gravando cada uma na saída padrão. Se ocorrer um erro durante a leitura, ele é capturado e registrado no console.

A propósito, você percebeu a vulnerabilidade de segurança no código acima?

Se você tivesse a extensão Snyk para VS Code instalada, veria um aviso ondulado do linter indicando uma vulnerabilidade de travessia de caminho no exemplo de código.

A extensão Snyk para VS Code mostraria como dados não confiáveis podem chegar a esse exemplo de código de back-end em Node.js, explicaria a vulnerabilidade e indicaria como corrigi-la com sugestões selecionadas por IA a partir de projetos de código aberto ativos.

A extensão Snyk para VS Code encontra uma vulnerabilidade de segurança no código de exemplo de backend Node.js.

2. Como usar o módulo Crypto para validar assinaturas de webhooks

Os webhooks se tornaram parte essencial do desenvolvimento moderno de back-end, pois permitem que diferentes serviços se comuniquem de forma eficiente e em tempo real. No entanto, o aumento do uso de webhooks também exige medidas de segurança mais robustas, como a validação de assinaturas de webhooks.

Em webhooks, uma assinatura é um hash enviado junto com o payload do webhook e calculado com uma chave secreta conhecida apenas pelo remetente e pelo destinatário. O destinatário pode calcular o hash por conta própria e compará-lo com a assinatura para verificar a autenticidade do webhook.

O módulo Crypto do Node.js oferece diversos recursos criptográficos, incluindo um conjunto de wrappers para as funções de hash, HMAC, cifra, decifra, assinatura e verificação do OpenSSL. O HMAC (código de autenticação de mensagem baseado em hash) é especialmente útil para validar assinaturas de webhooks.

Vamos analisar o trecho de código para entender como ele funciona:

const crypto = require("node:crypto");

const secret = process.env.WEBHOOK_SECERT;
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(
  hmac.update(request.rawBody).digest("hex"),
  "utf8"
);
const signature = Buffer.from(request.headers["x-signature"] || "", "utf8");

if (!crypto.timingSafeEqual(digest, signature)) {
  throw new Error("Invalid signature.");
}

Neste trecho, o método `crypto.createHmac` é usado para criar um objeto HMAC. Ele recebe dois parâmetros: o algoritmo a ser usado (neste caso, `sha256`) e a chave secreta.

Em seguida, o objeto HMAC é atualizado com o corpo bruto da solicitação do webhook usando o método `hmac.update`. Esse método pode ser chamado várias vezes, conforme novos dados são transmitidos.

O método `digest` é usado para gerar o hash. Ele só pode ser chamado uma vez no objeto HMAC e retorna o hash calculado. O parâmetro `hex` instrui o método a retornar o hash no formato hexadecimal.

Agora, vamos abordar a comparação segura de strings. O hash calculado (digest) e a assinatura recebida no cabeçalho da solicitação do webhook são comparados com o método `crypto.timingSafeEqual`. Ele faz uma comparação de igualdade entre dois buffers resistente a ataques de temporização, sendo ideal para comparar resultados criptográficos.

Ataques de temporização são um tipo de ataque de canal lateral em que um invasor tenta comprometer um sistema analisando o tempo necessário para executar algoritmos criptográficos. Ao usar um método seguro contra ataques de temporização, como `crypto.timingSafeEqual`, nos protegemos contra esse tipo de ataque. Se o digest e a assinatura não corresponderem, um erro é lançado, indicando que a solicitação do webhook pode não ser autêntica.

3. Como trabalhar com buffers e dados binários brutos no Node.js

A API Buffer do Node.js é uma ferramenta poderosa que permite trabalhar diretamente com dados binários. Seja para ler um arquivo, analisar uma imagem ou processar dados brutos, a API Buffer oferece métodos para realizar essas tarefas com eficiência.

Vamos conhecer alguns métodos comuns da API Buffer no Node.js, como `.from`, `.alloc` e `.write`, que permitem criar e manipular objetos buffer. O método `.from` cria um novo buffer com os dados recebidos como argumento, `.alloc` cria um buffer de tamanho especificado e `.write` permite gravar dados em um buffer. Outro método é `.concat`, usado para concatenar uma lista de instâncias de Buffer.

let buffer1 = Buffer.from('Hello, ');
let buffer2 = Buffer.from('World!');
let buffer3 = Buffer.concat([buffer1, buffer2]);

console.log(buffer3.toString());
// prints: 'Hello, World!'

Como analisar uma imagem com a API da OpenAI usando a API Buffer do Node.js

No próximo exemplo de código de back-end em Node.js, usamos a API Buffer para ler um arquivo de imagem e analisá-lo com a API da OpenAI. Primeiro, importamos os módulos necessários e criamos uma instância do cliente da API da OpenAI:

import { readFile } from "node:fs/promises";
import OpenAI from "openai";

const openai = new OpenAI();

Em seguida, lemos o arquivo de imagem para um buffer:

const imageBuffer = await readFileToBuffer(process.argv[2]);

Depois, validamos o tipo da imagem comparando a assinatura do arquivo com a assinatura conhecida de uma imagem PNG:

function isImageTypeValid(imageBuffer) {
  const pngSignature = Buffer.from([
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
  ]);
  const fileSignature = imageBuffer.slice(0, 8);

  if (pngSignature.equals(fileSignature)) {
    return true;
  }
}

Por fim, usamos a API da OpenAI para gerar um texto alternativo descritivo para a imagem:

async function generateAltTextForImage(imageBuffer) {
  const imageInBase64 = imageBuffer.toString("base64");

  const response = await openai.chat.completions.create({
    model: "gpt-4-vision-preview",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "What's in this image? generate a simple alt text for an image source in an HTML page",
          },
          {
            type: "image_url",
            image_url: {
              url: `data:image/png;base64,${imageInBase64}`,
            },
          },
        ],
      },
    ],
  });

  return response.choices[0];
}

Considerações de segurança ao usar a API Buffer

Ao trabalhar com a API Buffer, é importante ter em mente algumas considerações de segurança. O tratamento inadequado de dados binários pode causar vulnerabilidades, como erros de estouro ou esvaziamento de buffer. Valide sempre as entradas e trate os erros corretamente.

Além disso, fique atento a possíveis vulnerabilidades, como o construtor `Buffer`, que foi descontinuado e deve ser evitado em favor de alternativas mais seguras, como `Buffer.from` ou `Buffer.alloc`.

4. Como usar Symbol no JavaScript para encapsulamento

Symbols são um tipo primitivo de dado introduzido no ES6 (ECMAScript 2015) que representam identificadores únicos e imutáveis. Eles são criados com a função `Symbol()`, que pode receber uma descrição (uma string), útil para depuração, mas que não afeta a exclusividade do símbolo. Os Symbols são usados principalmente para criar chaves de propriedade exclusivas para objetos, que não entram em conflito com nenhuma outra propriedade, inclusive as herdadas. Isso os torna especialmente úteis para definir propriedades privadas ou especiais em objetos sem correr o risco de colisões entre nomes de propriedades.

Veja um exemplo de código de back-end em Node.js que mostra como usar Symbols para criar uma propriedade privada em uma classe e encapsular dados:

const _privateProperty = Symbol('privateProperty');

class MyClass {
  constructor(value) {
    this[_privateProperty] = value;
  }

  getPrivateProperty() {
    return this[_privateProperty];
  }
}

const instance = new MyClass('secret');
console.log(instance.getPrivateProperty());
// Will output 'secret'
// The _privateProperty cannot be directly accessed from outside the class

Além disso, não é possível acessar Symbols ao enumerar propriedades de um objeto (como em loops `for...in` ou com `Object.keys()`). Assim, eles podem simular propriedades e métodos privados em objetos, mas não são realmente privados: ainda é possível acessá-los com métodos de reflexão, como `Object.getOwnPropertySymbols()`.

O JavaScript define um conjunto de Symbols conhecidos que representam comportamentos internos da linguagem e podem ser personalizados por desenvolvedores. Por exemplo, veja como implementar um iterador para um objeto personalizado usando `Symbol.iterator`:

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
  }
};

for (const value of iterable) {
  // Logs 1 and 2
  console.log(value);
}

Como o Fastify usa Symbol no JavaScript

Vamos ver um exemplo mais próximo do mundo real com o framework de aplicações web Fastify e entender como o projeto usa Symbols. Especificamente, veremos um exemplo da arquitetura de plugins do Fastify. Um dos aspectos fundamentais do design do Fastify é o recurso de encapsulamento, que permite criar contextos de aplicação isolados usando plugins. Isso é essencial para criar aplicações de grande porte, nas quais colisões de namespaces podem se tornar um problema.

O Fastify usa Symbols para identificar propriedades e métodos internos de forma exclusiva, garantindo que eles não interfiram nas propriedades definidas pelo usuário nem nas de outros plugins. Veja um exemplo simplificado, baseado no uso real de Symbols no código-fonte do Fastify, para encapsular os metadados de um plugin:

const pluginMeta = Symbol('fastify.pluginMeta');

function registerPlugin(instance, plugin, options) {
  if (!plugin[pluginMeta]) {
    plugin[pluginMeta] = { options, name: plugin.name };
  }

  instance.register(plugin, options);
}

function myPlugin(instance, opts, done) {
  done();
}

registerPlugin(fastifyInstance, myPlugin, { prefix: '/api' });

No exemplo acima, `pluginMeta` é um Symbol usado pelo Fastify para associar metadados às funções de plugin. Esses metadados incluem as opções e o nome do plugin, além de outras informações que o framework possa precisar internamente. A função `registerPlugin` simplifica o processo de associar metadados a um plugin antes de registrá-lo em uma instância do Fastify. Esses metadados ficam acessíveis dentro do framework Fastify, mas isolados da interface pública do plugin e do escopo global da aplicação.

Usar Symbol no JavaScript para armazenar metadados internos traz várias vantagens em um framework como o Fastify:

  • Encapsulamento: impede que detalhes internos vazem para o espaço do usuário, mantendo a API pública limpa e intuitiva.

  • Segurança: reduz o risco de interferência acidental entre plugins ou entre um plugin e o núcleo do framework, já que Symbols não podem ser acessados por meio da enumeração normal de propriedades de objetos.

  • Clareza: diferencia claramente os mecanismos internos do framework das APIs disponíveis para desenvolvedores, facilitando a manutenção e a expansão do código-fonte do Fastify.

5. Template literals e tagged templates para gerar HTML e consultas SQL

Introduzidos no ES6, os template literals oferecem uma sintaxe mais legível e concisa para criar strings em JavaScript. Talvez você já os use para criar strings e incorporar expressões dinâmicas, como `hello ${name}`. Chega de concatenar strings.

Está cada vez mais comum usar template literals na forma de tagged templates para gerar HTML e consultas SQL em back-ends com Node.js. Veja dois exemplos reais em código de back-end e SSR com Node.js:

Templates com tags para gerar consultas SQL com a biblioteca PostgreSQL da Vercel

Lidar com consultas SQL no Node.js pode resultar em código longo e sujeito a erros, principalmente ao inserir valores dinamicamente nas consultas. O pacote PostgreSQL da Vercel (`@vercel/postgres`) oferece uma maneira mais segura e concisa de formatar consultas SQL usando literais de template:

import { sql } from '@vercel/postgres';

const jediName = 'Luke Skywalker';
const { rows } = await sql`SELECT * FROM jedis WHERE name = ${jediName};`;

A função de literal de template com tag SQL interpola com segurança a variável `jediName` na consulta SQL, ajudando a impedir ataques de injeção de SQL.

Templates com tags para gerar HTML em servidores Fastify

Assim como no caso da consulta SQL, o `fastify-html` é um plugin do Fastify que permite aos desenvolvedores usar templates com tags para gerar conteúdo HTML diretamente nos manipuladores de rotas. Isso pode ser especialmente útil para renderização no lado do servidor (SSR) ou para gerar conteúdo HTML dinâmico (alguém falou htmx?):

import fastify from 'fastify'
import fastifyHtml from 'fastify-html'

const app = fastify()
await app.register(fastifyHtml)

app.get('/', async (req, reply) => {
  const name = req.query.name || 'World';
  return reply.html`<h1>Hello ${name}</h1>`;
});

O que são literais de template com tag?

Com base no conceito de literais de template, os literais de template com tag são um recurso do JavaScript que permite definir uma função personalizada (a “tag”) para processar literais de template. Essa função recebe o literal de template como um array de strings e as expressões interpoladas como argumentos separados, permitindo manipular o template e seus valores antes da criação da string final.

Para concluir

Se você gostou deste artigo, confira também as boas práticas para criar um pacote npm moderno com segurança em mente e as 10 boas práticas para conteinerizar aplicações web Node.js com Docker.

E, se você gosta de histórias sobre segurança, prepare-se para combater ataques à cadeia de suprimentos no npm com as práticas de segurança para desenvolvedores que mencionei no artigo.

Por fim, não deixe de conferir o Snyk com uma conta gratuita para começar a proteger seu código Node.js, suas dependências e suas imagens de contêiner Docker.

Leia mais

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

Blog

Por que agentes de programação com IA continuam criando falhas de controle de acesso

Agentes de programação com IA podem gerar uma lógica de autorização que compila e passa pela revisão, mas permite que um tenant acesse os dados de outro. Saiba por que é difícil detectar falhas de controle de acesso e como evitá-las.