Zum Inhalt springen

2. npm - Pakete und Projekte

Zu Zen-Modus wechseln

Die Sprache allein ist wenig. JavaScript bringt von Haus aus keine umfangreiche Standardbibliothek mit: Es gibt kein eingebautes Werkzeug für Datumsrechnung mit Zeitzonen, keine Validierung, kein Testframework, keinen Datenbankzugriff. Was in anderen Sprachen mitgeliefert wird, kommt hier aus dem Ökosystem - und der Zugang dazu ist npm, der Paketmanager von Node.js.

Das hat zwei Seiten. Auf der einen steht die größte Sammlung wiederverwendbarer Software, die es gibt; kaum ein Problem, für das nicht bereits ein Paket existiert. Auf der anderen steht die Verantwortung, fremden Code in das eigene Projekt zu holen: Code, den niemand im Team gelesen hat, der eigene Abhängigkeiten mitbringt und der irgendwann nicht mehr gepflegt wird. Dieses Kapitel behandelt beides - die Werkzeuge und die Urteilsfähigkeit im Umgang mit ihnen.

Das Thema ist keine Nebensache: Jedes Projekt dieses Jahres beginnt damit, und die Fehlerbilder aus diesem Kapitel (“bei mir läuft es aber”) können Sie lange begleiten.

Ein JavaScript-Projekt beginnt mit einem Befehl:

Terminal-Fenster
npm init -y # -y accepts all defaults

Das Ergebnis ist eine einzige Datei, package.json. Sie ist die Beschreibung des Projekts, und sie ist maschinenlesbar - genau deshalb kann ein fremder Rechner aus ihr die Arbeitsumgebung wiederherstellen.

{
"name": "buffet-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node --watch src/main.js",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "^4.4.3"
},
"devDependencies": {
"typescript": "^5.9.2",
"vitest": "^4.1.10"
}
}

Die Felder im Einzelnen:

FeldBedeutung
name, versionKennung des Projekts; Pflicht, sobald ein Paket veröffentlicht wird
type"module" schaltet die import/export-Syntax frei (siehe Kapitel 1)
scriptsbenannte Befehle, aufrufbar mit npm run <name>
dependenciesPakete, die das laufende Programm braucht
devDependenciesPakete, die nur beim Entwickeln gebraucht werden

scripts ist das am meisten unterschätzte Feld. Es hält fest, wie das Projekt gestartet, getestet und geprüft wird - und zwar so, dass es niemand im Wiki nachschlagen muss:

Terminal-Fenster
npm run dev
npm run typecheck
npm test # test and start work without "run"

Ein neues Teammitglied klont das Repository, führt npm install aus und liest in scripts, welche Befehle es gibt. Das ist gelebte Projektdokumentation an der einzigen Stelle, an der sie nicht veraltet.

Terminal-Fenster
npm install zod # into dependencies
npm install --save-dev vitest # into devDependencies (short: -D)
npm install # restore everything from package.json
npm uninstall zod # remove

Jede Installation tut drei Dinge: Sie lädt das Paket in den Ordner node_modules, trägt es in die package.json ein und hält die exakt installierte Version in der package-lock.json fest.

Die Unterscheidung ist keine Formsache. Die Frage lautet: Braucht das fertige, laufende Programm dieses Paket?

  • Ein Validierungspaket, das bei jeder Anfrage die Eingaben prüft: dependencies.
  • Der TypeScript-Compiler, das Testframework, ein Code-Formatierer: devDependencies - auf dem Server läuft am Ende übersetztes JavaScript, kein Compiler.

Wer alles in dependencies wirft, schleppt beim Betrieb ein Vielfaches an Code mit; jede Zeile davon ist auch eine mögliche Sicherheitslücke.

  1. package.json - ja. Sie ist die Projektbeschreibung.

  2. package-lock.json - ja. Sie ist der Grund, warum bei allen dasselbe installiert wird.

  3. node_modules/ - nein. Der Ordner ist reproduzierbar, riesig und plattformabhängig; er gehört in die .gitignore.

Damit ist jedes geklonte Projekt mit einem einzigen npm install lauffähig.

An dieser Stelle entscheidet sich, ob ein Projekt in zwei Jahren noch baut. Pakete verwenden Semantic Versioning (SemVer), eine dreiteilige Nummer mit fester Bedeutung:

4 . 4 . 3
major minor patch
TeilWird erhöht beiFür Sie bedeutet das
patchFehlerbehebung, keine Schnittstellenänderunggefahrlos aktualisieren
minorneue Funktionen, bestehende bleiben gültiggefahrlos aktualisieren
majorÄnderung, die bestehenden Code brechen kannerst lesen, dann aktualisieren

Diese Bedeutung ist ein Versprechen der Paketautoren, keine technische Garantie. Es wird meistens gehalten, aber nicht immer - und ein major-Sprung ist immer eine bewusste Entscheidung, nie ein nebenbei ausgeführtes Kommando.

In der package.json steht deshalb selten eine exakte Version, sondern ein Bereich:

EintragBedeutung
"^4.4.3"alles ab 4.4.3 bis unter 5.0.0 (Standard bei npm install)
"~4.4.3"alles ab 4.4.3 bis unter 4.5.0 (nur Patches)
"4.4.3"exakt diese Version

Damit ergibt sich das Problem, das jeden Anfänger einmal erwischt: "^4.4.3" bedeutet, dass zwei Installationen an verschiedenen Tagen verschiedene Versionen ergeben können. Genau dafür existiert die package-lock.json: Sie hält fest, welche Version jedes einzelnen Pakets - auch jeder Abhängigkeit einer Abhängigkeit - tatsächlich installiert wurde.

flowchart LR
  A["package.json<br/>gewünschte Bereiche<br/>^4.4.3"] --> B["npm install"]
  B --> C["package-lock.json<br/>exakte Versionen<br/>4.4.3"]
  B --> D["node_modules/"]
  C -->|"npm ci"| D

Für automatisierte Umgebungen (die CI-Pipeline aus Kapitel 13) gibt es den strengeren Befehl:

Terminal-Fenster
npm ci # installs exactly what the lock file says; fails if it is missing or inconsistent

npm ci löscht node_modules und baut die Umgebung ausschließlich aus der Sperrdatei auf. Er ist damit reproduzierbar und schneller - und er ist der Grund, warum die Sperrdatei ins Repository gehört.

Terminal-Fenster
npm outdated # what is out of date?
npm update # update within the allowed range (no major jumps)
npm install zod@5 # deliberate major jump

npm update bleibt innerhalb der Bereiche aus der package.json und ist deshalb der ungefährliche Befehl. Ein major-Sprung wird einzeln gemacht, mit einem Blick in die Änderungsliste des Pakets und einem Testlauf danach - nie für mehrere Pakete gleichzeitig, weil sonst unklar bleibt, welcher Sprung etwas gebrochen hat.

Manche Werkzeuge braucht man genau einmal - etwa den Projektgenerator eines Frameworks. npx lädt ein Paket, führt es aus und installiert es nicht dauerhaft:

Terminal-Fenster
npx create-next-app@latest my-project # used again in chapter 8

Für Werkzeuge, die im Projekt stehen, ruft npx die lokale Version auf (npx tsc nimmt den TypeScript-Compiler aus node_modules, nicht irgendeinen global installierten). Das ist der Grund, warum Projektwerkzeuge lokal und nicht global installiert werden: Zwei Projekte können unterschiedliche Compilerversionen brauchen, und global geht immer nur eine.

Ein Paket zu installieren ist eine Entscheidung mit Folgen: Sie übernehmen Code, den Sie nicht geschrieben haben, in Ihr Produkt - samt dessen Abhängigkeiten und dessen Sicherheitslücken.

Prüffragen vor jedem npm install:

  1. Brauche ich das wirklich? Für drei Zeilen Datumsformatierung braucht es kein Paket. Jede Abhängigkeit ist dauerhafte Wartungslast.
  2. Wird es gepflegt? Wann kam die letzte Veröffentlichung? Wie viele offene Fehlermeldungen liegen unbeantwortet?
  3. Wie verbreitet ist es? Wöchentliche Downloadzahlen sind ein grober, aber brauchbarer Hinweis - vieles bricht auf, bevor es Sie trifft.
  4. Was schleppt es mit? Ein Paket mit fünfzig eigenen Abhängigkeiten ist eine andere Entscheidung als eines ohne.
  5. Passt die Lizenz? Bei schulischen Projekten selten kritisch, im Berufsleben regelmäßig.
Terminal-Fenster
npm audit # known vulnerabilities in the dependency tree
npm audit fix # update what can be updated safely
npm ls zod # who actually pulls in this package?

npm ist nicht das einzige Werkzeug seiner Art. Sie werden in fremden Projekten auf yarn, pnpm und bun stoßen; alle lesen dieselbe package.json, unterscheiden sich aber in Geschwindigkeit, Plattenverbrauch und eigener Sperrdatei (yarn.lock, pnpm-lock.yaml, bun.lock).

Zwei Regeln genügen fürs Erste: In einem Projekt wird ein Paketmanager verwendet - zwei Sperrdateien nebeneinander sind ein handfester Fehler. Und welcher es ist, erkennen Sie an der vorhandenen Sperrdatei, nicht an der Gewohnheit. Im Unterricht bleiben wir bei npm, weil es mit Node.js mitkommt und keine Zusatzinstallation braucht.

  1. node_modules/ eingecheckt: bläht das Repository um Zehntausende Dateien auf. In die .gitignore, bevor der erste Commit passiert.
  2. package-lock.json ignoriert oder gelöscht: die Ursache hinter den meisten “bei mir läuft es aber”-Fällen.
  3. Alles in dependencies: Testframework und Compiler landen im Betrieb; unnötiger Code und unnötige Angriffsfläche.
  4. Global statt lokal installiert: funktioniert auf dem eigenen Rechner und nirgends sonst; das Projekt ist ohne die Dokumentation dieser Handgriffe nicht lauffähig.
  5. Fehlendes "type": "module": import schlägt mit Cannot use import statement outside a module fehl.
  6. npm audit fix --force blind ausgeführt: Das --force erlaubt major-Sprünge und bricht dabei gern das Projekt. Erst lesen, dann entscheiden.

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Anwenden: ein Node.js-Projekt anlegen, Pakete installieren und eigene Skripte in der package.json definieren und ausführen.
  • Erklären: Aufbau und Zweck der package.json beschreiben und dependencies von devDependencies unterscheiden.
  • Erklären: Semantic Versioning erklären und die Bedeutung von ^ und ~ angeben.
  • Erklären: die Aufgabe der Sperrdatei erklären und begründen, warum sie ins Repository gehört und node_modules/ nicht.
  • Anwenden: eine Projektumgebung mit npm install beziehungsweise npm ci reproduzierbar wiederherstellen.
  • Beurteilen: vor der Aufnahme einer Abhängigkeit deren Notwendigkeit, Pflegezustand und Risiko beurteilen.
  • Aufgabe 01 - TypeScript-Spickzettel