Skip to main content

Unverzichtbare Node.js-Backend-Beispiele für Entwickler im Jahr 2024

Artikel von

12. Juni 2024

0 Min. Lesezeit

Die Node.js-Backend-Entwicklung behauptet sich auch 2024 als leistungsstarke und flexible Laufzeitumgebung für skalierbare und effiziente Anwendungen – umso mehr angesichts des Aufkommens anderer Laufzeitumgebungen wie Bun.

In diesem Artikel möchte ich eine kompakte Einführung in unverzichtbare Node.js-Backend-Beispiele geben, die den effektiven Einsatz fortgeschrittener JavaScript- und Node.js-Funktionen veranschaulichen. Von der Nutzung des WHATWG Streams Standard und der Web Streams API für eine effiziente Datenverarbeitung über den Einsatz des integrierten Node.js-Crypto-Moduls für die Sicherheit und die Verarbeitung von Binärdaten mit Buffers bis hin zur Verwendung von Symbols zur Kapselung und für Namespaces sowie Template-Literalen und Tagged Templates zum Erzeugen dynamischer HTML- und SQL-Abfragen – jeder Abschnitt bietet praxisnahe Code-Snippets und Einblicke aus der realen Welt.

Diese Beispiele zeigen nicht nur die Vielseitigkeit und Leistungsfähigkeit von Node.js bei der Lösung von Backend-Herausforderungen, sondern dienen Entwicklern auch als wertvolle Referenz, um ihre Backend-Lösungen 2024 auf ein neues Niveau zu heben.

Kapitel in diesem Artikel:

  1. Der WHATWG Streams Standard, die Web Streams API und asynchrone Iterables

  2. Arbeiten mit dem Crypto-Modul zur Validierung von Webhook-Signaturen

  3. Arbeiten mit Buffers und rohen Binärdaten in Node.js

  4. JavaScript Symbol zur Kapselung verwenden

  5. Template-Literale und Tagged Templates zum Erzeugen von HTML- und SQL-Abfragen

Die vollständigen Node.js-Backend-Beispiele im Quellcode finden Sie jederzeit in diesem GitHub-Repository.

1. Der WHATWG Streams Standard, die Web Streams API und Async Iterables

2024 hat die Nutzung von Streams deutlich zugenommen, insbesondere bei der Arbeit mit großen Sprachmodellen für generative künstliche Intelligenz (GenAI). Das OpenAI SDK für Chat-Abschlüsse ist ein Paradebeispiel für diesen Trend. Ein typisches Beispiel für das Streamen von Daten über die OpenAI-SDK-API sieht so aus:

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

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

Hier nutzen wir das Konzept asynchroner Iterables, das asynchrone Workflows in Node.js vereinfacht und an den Promise-basierten Programmierstil anpasst. Die Anweisung `for await…` erstellt eine Schleife, die sowohl asynchrone Iterable-Objekte als auch synchrone Iterables durchläuft, darunter die integrierten Typen String, Array, Array-ähnliche Objekte (z. B. arguments oder NodeList), TypedArray, Map, Set sowie benutzerdefinierte asynchrone und synchrone Iterables.

Einführung der WHATWG-Standard-Web-Streams in Node.js

Web Streams sind Teil des WHATWG Streams Standard und in Node.js integriert. Sie bieten eine robuste Möglichkeit, Streaming-Daten zu verarbeiten. Entwickler können mit diesem Standard Streaming-Daten effizient mit JavaScript lesen, schreiben und transformieren.

Sehen wir uns ein vollständiges Beispiel für die Verwendung von Web Streams in Node.js an.

Im folgenden Beispiel erstellen wir eine Funktion `createReadableStreamFromFile()`, die die Klasse `ReadableStream` aus der Web Streams API verwendet, um einen Datenstream aus einer Datei zu erzeugen. Eine zweite Funktion, `consumeStreamWithAsyncIterator()`, verarbeitet diesen Stream anschließend mit einem Async Iterator.

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);

In diesem Code verwenden wir die Web Streams API, um Daten kontinuierlich aus einer Datei zu lesen. Die Funktion `createReadableStreamFromFile()` gibt für einen angegebenen Dateipfad ein `ReadableStream`-Objekt zurück. Dieser Stream wird anschließend von `consumeStreamWithAsyncIterator()` verarbeitet. Die Funktion liest den Stream Block für Block und schreibt jeden Block in die Standardausgabe. Tritt beim Lesevorgang ein Fehler auf, wird er abgefangen und in der Konsole protokolliert.

Übrigens: Ist Ihnen die Sicherheitslücke im obigen Code aufgefallen?

Wenn Sie die Snyk VS Code-Erweiterung installiert hätten, würde Sie ein gewellter Linter-Fehler auf eine Path-Traversal-Sicherheitslücke im Codebeispiel hinweisen.

Die Snyk VS Code-Erweiterung würde Ihnen zeigen, wie unsichere Daten in dieses Node.js-Backend-Codebeispiel gelangen, worin die Sicherheitslücke besteht und wie Sie sie mit KI-kuratierten Vorschlägen aus aktuellen Open-Source-Projekten beheben können.

Die Snyk-VS-Code-Erweiterung findet eine Sicherheitslücke in meinem Beispielcode für ein Node.js-Backend.

2. Arbeiten mit dem Crypto-Modul zur Validierung von Webhook-Signaturen

Webhooks sind ein fester Bestandteil der modernen Backend-Entwicklung. Sie ermöglichen es verschiedenen Diensten, effizient und in Echtzeit miteinander zu kommunizieren. Mit der zunehmenden Nutzung von Webhooks steigt jedoch auch der Bedarf an stärkeren Sicherheitsmaßnahmen. Eine davon ist die Validierung von Webhook-Signaturen.

Bei Webhooks ist eine Signatur ein Hash, der zusammen mit der Webhook-Nutzlast gesendet wird. Er wird mithilfe eines geheimen Schlüssels berechnet, der nur dem Absender und dem Empfänger bekannt ist. Der Empfänger kann den Hash seinerseits berechnen und mit der Signatur vergleichen, um die Authentizität des Webhooks zu überprüfen.

Das Node.js-Crypto-Modul bietet zahlreiche kryptografische Funktionen, darunter Wrapper für die Hash-, HMAC-, Cipher-, Decipher-, Sign- und Verify-Funktionen von OpenSSL. HMAC (Hash-based Message Authentication Code) eignet sich besonders gut zur Validierung von Webhook-Signaturen.

Sehen wir uns das bereitgestellte Code-Snippet genauer an, um seine Funktionsweise zu verstehen:

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.");
}

In diesem Snippet wird mit der Methode `crypto.createHmac` ein HMAC-Objekt erstellt. Die Methode erwartet zwei Parameter: den zu verwendenden Algorithmus (in diesem Fall `sha256`) und den geheimen Schlüssel.

Anschließend wird das HMAC-Objekt mithilfe der Methode `hmac.update` mit dem rohen Body der Webhook-Anfrage aktualisiert. Diese Methode kann beim Streamen mehrmals mit neuen Daten aufgerufen werden.

Anschließend wird mit der Methode `digest` der Hash erzeugt. Sie kann für das HMAC-Objekt nur einmal aufgerufen werden und gibt den berechneten Hash zurück. Der Parameter `hex` weist die Methode an, den Hash im Hexadezimalformat zurückzugeben.

Nun kommt der Vergleich von Strings auf sichere Weise ins Spiel. Der berechnete Hash (Digest) und die empfangene Signatur aus dem Header der Webhook-Anfrage werden mit der Methode `crypto.timingSafeEqual` verglichen. Diese Methode vergleicht zwei Buffers auf Gleichheit und schützt dabei vor Timing-Angriffen. Damit eignet sie sich ideal für den Vergleich kryptografischer Ausgaben.

Timing-Angriffe sind eine Art Seitenkanalangriff, bei dem ein Angreifer versucht, ein System zu kompromittieren, indem er die Ausführungszeit kryptografischer Algorithmen analysiert. Mit einer timing-sicheren Methode wie `crypto.timingSafeEqual` schützen wir uns vor solchen Angriffen. Stimmen Digest und Signatur nicht überein, wird ein Fehler ausgelöst, der darauf hinweist, dass die Webhook-Anfrage möglicherweise nicht authentisch ist.

3. Arbeiten mit Buffers und rohen Binärdaten in Node.js

Die Buffer-API in Node.js ist ein leistungsstarkes Werkzeug, mit dem Entwickler direkt mit Binärdaten arbeiten können. Ganz gleich, ob Sie eine Datei lesen, ein Bild analysieren oder Rohdaten verarbeiten möchten: Die Buffer-API bietet Methoden, mit denen sich solche Aufgaben effizient erledigen lassen.

Sehen wir uns einige gängige Methoden der Buffer-API in Node.js an, etwa `.from`, `.alloc` und `.write`. Mit ihnen lassen sich Buffer-Objekte erstellen und bearbeiten. Die Methode `.from` erstellt einen neuen Buffer aus den als Argument übergebenen Daten, `.alloc` erstellt einen Buffer in einer vorgegebenen Größe und mit `.write` können Sie Daten in einen Buffer schreiben. Eine weitere Methode ist `.concat`, mit der sich eine Liste von Buffer-Instanzen verketten lässt.

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

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

Ein Bild mit der OpenAI API und der Node.js Buffer-API analysieren

Im nächsten Node.js-Backend-Codebeispiel verwenden wir die Buffer-API, um eine Bilddatei einzulesen und mithilfe der OpenAI API zu analysieren. Zunächst importieren wir die erforderlichen Module und erstellen eine Instanz des OpenAI-API-Clients:

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

const openai = new OpenAI();

Als Nächstes lesen wir die Bilddatei in einen Buffer ein:

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

Anschließend überprüfen wir den Bildtyp, indem wir die Dateisignatur mit einer bekannten PNG-Signatur abgleichen:

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;
  }
}

Zum Schluss erzeugen wir mithilfe der OpenAI API einen beschreibenden Alt-Text für das Bild:

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];
}

Sicherheitsaspekte bei der Verwendung der Buffer-API

Bei der Arbeit mit der Buffer-API sollten Sie einige Sicherheitsaspekte beachten. Der unsachgemäße Umgang mit Binärdaten kann zu Sicherheitslücken wie Buffer-Overflow- oder Buffer-Underflow-Fehlern führen. Validieren Sie Eingaben stets und behandeln Sie Fehler ordnungsgemäß.

Achten Sie außerdem auf potenzielle Sicherheitslücken, etwa im Zusammenhang mit dem Konstruktor `Buffer`, der inzwischen veraltet ist und zugunsten sichererer Alternativen wie `Buffer.from` oder `Buffer.alloc` vermieden werden sollte.

4. JavaScript Symbol zur Kapselung verwenden

Symbols sind ein primitiver Datentyp, der mit ES6 (ECMAScript 2015) eingeführt wurde und eindeutige, unveränderliche Bezeichner darstellt. Sie werden mit der Funktion `Symbol()` erstellt, die optional eine Beschreibung (einen String) akzeptiert. Diese kann beim Debugging hilfreich sein, hat aber keinen Einfluss auf die Eindeutigkeit des Symbols. Symbols werden vor allem verwendet, um eindeutige Eigenschaftsschlüssel für Objekte zu erstellen, die nicht mit anderen Eigenschaften – auch geerbten – kollidieren. Dadurch eignen sie sich besonders gut, um private oder spezielle Objekteigenschaften zu definieren, ohne Namenskollisionen zu riskieren.

Hier sehen Sie ein Node.js-Backend-Codebeispiel, das zeigt, wie Sie mit Symbols eine private Eigenschaft in einer Klasse erstellen und so Daten kapseln:

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

Außerdem sind Symbols über die Aufzählung von Objekteigenschaften (etwa mit `for...in`-Schleifen oder `Object.keys()`) nicht zugänglich. Sie können also gewissermaßen private Eigenschaften und Methoden für Objekte simulieren. Wichtig ist jedoch, dass sie nicht wirklich privat sind und sich weiterhin über Reflection-Methoden wie `Object.getOwnPropertySymbols()` abrufen lassen.

JavaScript definiert eine Reihe bekannter Symbols, die interne Sprachverhaltensweisen darstellen und von Entwicklern angepasst werden können. Ein Beispiel ist die Implementierung eines Iterators für ein benutzerdefiniertes Objekt mit `Symbol.iterator`:

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

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

So verwendet Fastify JavaScript Symbol

Sehen wir uns ein praxisnäheres Beispiel mit dem Fastify-Webanwendungsframework und der Verwendung von Symbols im Projekt an. Konkret betrachten wir ein Beispiel aus der Plugin-Architektur von Fastify. Ein zentrales Merkmal des Fastify-Designs ist die Kapselung: Mit Plugins können Entwickler isolierte Anwendungskontexte erstellen. Das ist für die Entwicklung großer Anwendungen entscheidend, in denen Namenskollisionen zum Problem werden können.

Fastify verwendet Symbols, um interne Eigenschaften und Methoden eindeutig zu kennzeichnen. So wird verhindert, dass sie mit benutzerdefinierten Eigenschaften oder denen anderer Plugins interferieren. Hier sehen Sie ein vereinfachtes Beispiel, das auf der tatsächlichen Verwendung von Symbols im Fastify-Quellcode basiert und Metadaten eines Plugins kapselt:

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' });

Im obigen Beispiel ist `pluginMeta` ein Symbol, mit dem Fastify Metadaten an Plugin-Funktionen anfügt. Diese Metadaten enthalten Optionen und den Namen des Plugins sowie möglicherweise weitere Informationen, die das Framework intern benötigt. Die Funktion `registerPlugin` vereinfacht das Anfügen von Metadaten an ein Plugin, bevor es bei einer Fastify-Instanz registriert wird. Diese Metadaten sind anschließend innerhalb des Fastify-Frameworks verfügbar, bleiben jedoch von der öffentlichen Schnittstelle des Plugins und dem globalen Anwendungsbereich getrennt.

Die Verwendung von JavaScript Symbol für interne Metadaten bietet in einem Framework wie Fastify mehrere Vorteile:

  • Kapselung: Interne Details gelangen nicht in den Benutzerbereich. Dadurch bleibt die öffentliche API übersichtlich und intuitiv.

  • Sicherheit: Das Risiko unbeabsichtigter Konflikte zwischen Plugins oder zwischen einem Plugin und dem Kern-Framework sinkt, da Symbols nicht über die normale Aufzählung von Objekteigenschaften zugänglich sind.

  • Klarheit: Interne Mechanismen des Frameworks lassen sich klar von den APIs unterscheiden, die Entwicklern zur Verfügung stehen. Das erleichtert die Wartung und Erweiterung der Fastify-Codebasis.

5. Template-Literale und Tagged Templates zum Erzeugen von HTML- und SQL-Abfragen

Mit ES6 wurden Template-Literale eingeführt. Sie bieten eine besser lesbare und kompaktere Syntax zum Erzeugen von Strings in JavaScript. Vielleicht verwenden Sie Template-Literale bereits, um Strings zu schreiben und dynamische Ausdrücke einzubinden – etwa `hello ${name}`. Die String-Verkettung gehört damit der Vergangenheit an.

Zunehmend werden Template-Literale in Form von Tagged Templates verwendet, um HTML- und SQL-Abfragen in Node.js-Backends zu erzeugen. Hier sind zwei praxisnahe Beispiele aus Node.js-Backend- und SSR-Code:

Tagged Templates zum Generieren von SQL-Abfragen mit der PostgreSQL-Bibliothek von Vercel

Der Umgang mit SQL-Abfragen in Node.js führt oft zu ausführlichem und fehleranfälligem Code, insbesondere wenn Werte dynamisch in Abfragen eingefügt werden. Das PostgreSQL-Paket von Vercel (`@vercel/postgres`) bietet eine sicherere und prägnantere Möglichkeit, SQL-Abfragen mithilfe von Template-Literalen zu formatieren:

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

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

Die SQL-getaggte Template-Literal-Funktion fügt die Variable `jediName` sicher in die SQL-Abfrage ein und verhindert dadurch SQL-Injection-Angriffe.

Tagged Templates zum Generieren von HTML auf Fastify-Servern

Ähnlich wie bei SQL-Abfragen ermöglicht `fastify-html`, ein Fastify-Plugin, Entwicklerinnen und Entwicklern, Tagged Templates zu verwenden, um HTML-Inhalte direkt in Routen-Handlern zu generieren. Das kann besonders beim serverseitigen Rendering (SSR) oder beim Generieren dynamischer HTML-Inhalte nützlich sein (hat jemand htmx gesagt?):

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>`;
});

Was sind getaggte Template-Literale?

Aufbauend auf dem Konzept der Template-Literale sind getaggte Template-Literale ein JavaScript-Feature, mit dem Sie eine benutzerdefinierte Funktion (das „Tag“) definieren können, die Template-Literale verarbeitet. Diese Funktion erhält das Template-Literal als Array von Zeichenfolgen und die interpolierten Ausdrücke als separate Argumente. So können Sie das Template und seine Werte bearbeiten, bevor die endgültige Zeichenfolge erstellt wird.

Zum Abschluss

Wenn Ihnen dieser Artikel gefallen hat, interessieren Sie sich vielleicht auch für Best Practices für die Erstellung eines modernen npm-Pakets mit Blick auf die Sicherheit und 10 Best Practices, um Node.js-Webanwendungen mit Docker zu containerisieren.

Und wenn Sie sich für Security-Geschichten interessieren, sollten Sie sich mit den im Artikel erwähnten Security-Praktiken für Entwicklerinnen und Entwickler gut darauf vorbereiten, Supply-Chain-Angriffe auf npm zu bekämpfen.

Vergessen Sie zum Schluss nicht, Snyk mit einem kostenlosen Konto auszuprobieren, um Ihren Node.js-Code, Ihre Abhängigkeiten und Ihre Docker-Container-Images zu schützen.

Weiterlesen

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.

feature insights context
Blog

Autonome Angriffe sind bereits Realität. Die Verteidigung muss Schritt halten.

Autonome Angreifer verkürzen das Zeitfenster für die Verteidigung. Erfahren Sie, wie kontinuierliches Erkennen, Beheben, Validieren und Verhindern Sicherheitsteams helfen kann, Schritt zu halten.

Blog

Warum KI-Coding-Agenten immer wieder fehlerhafte Zugriffskontrollen schreiben

KI-Coding-Agenten können Autorisierungslogik erzeugen, die kompiliert und die Prüfung besteht, dabei aber die Daten eines Mandanten für einen anderen offenlegt. Erfahren Sie, warum fehlerhafte Zugriffskontrollen schwer zu erkennen sind und wie Sie sie verhindern.