Zum Inhalt springen
Node.js Anfรคnger 25 min

npm & Module verstehen

package.json, Dependencies, semver und das ESM/CommonJS-Dilemma. Alles ueber das npm-Oekosystem und wie du eigene Module baust.

Aktualisiert:
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 CJS
  • main - Haupt-Eintrittspunkt
  • scripts - CLI-Kommandos
  • dependencies - Prod-Abhaengigkeiten
  • devDependencies - nur fuer Entwicklung
  • engines - 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.x
  • 1.2.3 - exakt diese Version
  • * oder latest - 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:

  1. Auf npmjs.com registrieren
  2. npm login
  3. package.json vervollstaendigen (main, files, exports)
  4. 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 audit regelmaessig 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.json ist das Projekt-Herz: scripts, dependencies, type
  • SemVer: ^ fuer minor-kompatibel, ~ fuer patch-kompatibel
  • ESM mit "type": "module" und import/export ist modern
  • .js-Endung beim Import ist in ESM Pflicht
  • package-lock.json immer committen
  • node_modules nie committen
  • npm audit fuer Security-Checks

Im naechsten Kapitel: async/await - das Herzstueck von asynchronem Node-Code.

Zurรผck zum Node.js Kurs