npm & Module verstehen
package.json, Dependencies, semver und das ESM/CommonJS-Dilemma. Alles ueber das npm-Oekosystem und wie du eigene Module baust.
Inhaltsverzeichnis
npm & Module verstehen
Ein Node-Projekt besteht aus Modulen - deinen eigenen und Tausenden aus dem Paket-Oekosystem. Zeit, die Grundlagen zu klaeren.
package.json - das Projekt-Manifest
Jedes Node-Projekt hat eine package.json im Root:
{
"name": "meine-app",
"version": "1.0.0",
"description": "Eine kleine App",
"type": "module",
"main": "index.js",
"scripts": {
"dev": "node --watch index.js",
"start": "node index.js",
"test": "vitest",
"lint": "eslint ."
},
"dependencies": {
"express": "^4.21.0"
},
"devDependencies": {
"vitest": "^2.0.0",
"typescript": "^5.6.0"
},
"engines": {
"node": ">=22"
}
}
Felder erklaert
name- Paketname (bei Publish wichtig)version- aktuelle Version (SemVer)type-"module"fuer ESM,"commonjs"fuer CJSmain- Haupt-Eintrittspunktscripts- CLI-Kommandosdependencies- Prod-AbhaengigkeitendevDependencies- nur fuer Entwicklungengines- welche Node-Versionen sind OK
Pakete installieren
npm install express # als dependency
npm install -D typescript # als devDependency
npm install -g pnpm # global
Kurzformen: npm i, npm i -D.
package-lock.json
Wenn du npm install ausfuehrst, entsteht eine package-lock.json. Sie haelt exakte Versionen aller (transitive) Dependencies fest.
Immer mit-committen! So stellst du sicher, dass alle genau die gleichen Versionen bekommen.
Semantic Versioning (SemVer)
Versionen haben die Form MAJOR.MINOR.PATCH:
^1.2.3- kompatibel mit 1.x.y (Default bei npm install)~1.2.3- kompatibel mit 1.2.x1.2.3- exakt diese Version*oderlatest- alles (gefaehrlich)
Regeln von SemVer:
- PATCH: Bugfixes, keine API-Aenderung
- MINOR: neue Features, abwaertskompatibel
- MAJOR: breaking changes
Scripts ausfuehren
npm run dev # laeuft "dev"-Script
npm start # "start" - ohne "run"
npm test # "test" - ohne "run"
npm run # listet alle verfuegbaren Scripts
Scripts verketten
{
"scripts": {
"clean": "rm -rf dist",
"build": "tsc",
"prebuild": "npm run clean",
"start": "npm run build && node dist/server.js"
}
}
prebuild laeuft automatisch vor build - ein Life-Cycle-Hook.
npx - npm-Paket direkt ausfuehren
npx create-react-app meine-app # installiert und laeuft einmalig
npx tsx skript.ts # TypeScript direkt ausfuehren
npx installiert das Paket (wenn noetig) und fuehrt es aus, ohne global zu installieren.
ESM vs. CommonJS
Node unterstuetzt zwei Module-Systeme:
CommonJS (alt)
const fs = require("node:fs");
const { readFile } = require("node:fs/promises");
function gruessen(name) {
return `Hallo, ${name}!`;
}
module.exports = { gruessen };
ESM (modern, empfohlen)
import fs from "node:fs";
import { readFile } from "node:fs/promises";
export function gruessen(name) {
return `Hallo, ${name}!`;
}
Welche nimmst du? Fuer neue Projekte: ESM. Setze "type": "module" in package.json.
CJS und ESM in einem Projekt
- ESM-Datei kann CJS importieren:
import pkg from "cjs-pkg" - CJS kann ESM nur via
import()(async) nutzen - Das ist der Hauptgrund fuer โModule-Hoelleโ
Eigene Module
Einfache Datei-Module:
math.js:
export function addiere(a, b) {
return a + b;
}
export function verdopple(n) {
return n * 2;
}
export default function quadriere(n) {
return n * n;
}
index.js:
import quadriere, { addiere, verdopple } from "./math.js";
console.log(addiere(3, 4)); // 7
console.log(verdopple(5)); // 10
console.log(quadriere(6)); // 36
Wichtig in ESM: Dateiendung (.js) ist Pflicht beim Import.
Pfade mit Alias
Bei grossen Projekten wird ../../../../utils/logger.js nervig. Mit imports in package.json:
{
"imports": {
"#utils/*": "./src/utils/*.js",
"#db": "./src/db/index.js"
}
}
Dann:
import { logger } from "#utils/logger";
import db from "#db";
Sauberer als relative Pfade.
node_modules - das Verzeichnis
Installierte Pakete landen in node_modules/. Es kann huge werden (hunderte MB).
Nicht ins Git committen - stattdessen .gitignore:
node_modules/
dist/
.env
Dependencies aktualisieren
npm outdated # was ist veraltet?
npm update # Updates im erlaubten Range (SemVer)
npm install express@latest # explizit auf latest
Fuer groessere Aufraeumaktionen: npm-check-updates:
npx npm-check-updates
npx npm-check-updates -u # updated package.json
npm install
Security-Audit
npm prueft regelmaessig auf bekannte Luecken:
npm audit
npm audit fix # automatische Fixes
npm audit fix --force # auch Major-Updates
Eigenes Paket veroeffentlichen
Kurz-Anleitung:
- Auf npmjs.com registrieren
npm loginpackage.jsonvervollstaendigen (main, files, exports)npm publish
Dein Paket ist sofort global installierbar mit npm install dein-paket.
Workspaces - Monorepos
Mehrere Pakete in einem Repo:
package.json im Root:
{
"name": "monorepo",
"private": true,
"workspaces": [
"packages/*"
]
}
Dann hast du packages/core, packages/web, packages/api - alle mit eigenen package.json, aber gemeinsamem node_modules.
pnpm und bun haben besseres Workspace-Handling als npm.
Die Dependency-Falle
Ein Problem vom npm-Oekosystem: Viele Mini-Pakete. Das fuehrt zu:
- Tausenden transitiven Dependencies
- Potenzielle Security-Luecken
- Slow Installs
Tipps:
npm auditregelmaessig pruefen- Wenige, grosse Libs bevorzugen (z.B. Lodash) statt 20 Mikro-Pakete
- Pakete pruefen bevor du sie addest (GitHub-Stars, Issues, letzte Updates)
Praktisches Beispiel
package.json:
{
"name": "todo-cli",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node --watch src/index.js"
},
"dependencies": {
"chalk": "^5.3.0"
}
}
src/index.js:
import chalk from "chalk";
import { readFile, writeFile } from "node:fs/promises";
const [cmd, ...args] = process.argv.slice(2);
const datei = "todos.json";
async function load() {
try {
return JSON.parse(await readFile(datei, "utf8"));
} catch {
return [];
}
}
async function save(todos) {
await writeFile(datei, JSON.stringify(todos, null, 2));
}
const todos = await load();
if (cmd === "add") {
todos.push({ text: args.join(" "), erledigt: false });
await save(todos);
console.log(chalk.green("Hinzugefuegt."));
} else if (cmd === "list") {
todos.forEach((t, i) => {
const prefix = t.erledigt ? chalk.gray("[x]") : chalk.yellow("[ ]");
console.log(`${prefix} ${i}: ${t.text}`);
});
} else if (cmd === "done") {
todos[parseInt(args[0])].erledigt = true;
await save(todos);
console.log(chalk.green("Erledigt."));
} else {
console.log("Nutzung: node src/index.js add <text> | list | done <index>");
}
Zusammenfassung
package.jsonist das Projekt-Herz: scripts, dependencies, type- SemVer:
^fuer minor-kompatibel,~fuer patch-kompatibel - ESM mit
"type": "module"undimport/exportist modern .js-Endung beim Import ist in ESM Pflichtpackage-lock.jsonimmer committennode_modulesnie committennpm auditfuer Security-Checks
Im naechsten Kapitel: async/await - das Herzstueck von asynchronem Node-Code.