Private npm- und NuGet-Packages: Naming, Versioning, Feeds und Best Practices
Sobald Shared Code über Team- oder Repo-Grenzen hinweg genutzt wird, werden Copy-Paste und lose Shared-Ordner schnell teuer. Jede Änderung bedeutet dann Koordination, Abstimmung und Risiko.
Interne Packages sind oft der pragmatische Ausweg, weil sie Shared Code wie ein kleines Produkt behandeln mit klarer API (Schnittstelle), Versionsnummern, Ownership und einem Release-Prozess. Sie eignen sich sowohl für rein interne Pakete, die nur innerhalb eines Teams oder Produkts verteilt werden, als auch für Inner Source. Inner Source bedeutet, dass Code innerhalb der Organisation offen zugänglich und gemeinschaftlich entwickelt wird. Das ist ähnlich zu Open Source in Arbeitsweisen und Transparenz, aber mit eingeschränkter Sichtbarkeit und Governance, die auf die Unternehmensumgebung beschränkt ist.
Warum interne Packages oft besser sind als Shared Projects
Ein Monorepo (mehrere Projekte in einem Repo) mit Project References (direkten Projekt-Abhängigkeiten im Build) ist bequem, solange ihr gemeinsam released und dieselbe technische Klammer habt. Sobald Teams unabhängig deployen oder du Breaking Changes kontrolliert ausrollen willst, sind Packages meist die bessere Abstraktion.
Das liegt an einem einfachen Effekt: Ein Package zwingt dich zu einem expliziten Vertrag. Du veröffentlichst eine Version, andere Teams konsumieren sie, und du musst Kompatibilität und Deprecation (als veraltet markieren) aktiv managen. Für das Thema Publishing im Unternehmen sind Azure Artifacts Feeds und GitHub Packages die typischen Einstiegspunkte.
Package-Design: API Surface, Dependencies, Compatibility
Fokussiere beim Package-Design auf einen kleinen, klaren API Surface. Alles, was nicht Teil des Vertrags sein muss, bleibt ein Implementierungsdetail. Das reduziert Breaking Changes und macht dein Package langfristig wartbar.
Zwei typische Stolpersteine sind transitive Dependencies und Zielplattformen. Transitive Dependencies sind Abhängigkeiten, die du indirekt mitziehst, weil eine deiner Libraries sie benötigt.
Wenige, bewusst gewählte Dependencies sind fast immer besser als ein Convenience-Wrapper über zehn Libraries. Und bei der Kompatibilität solltest du früh entscheiden, welche Targets du unterstützen willst, in .NET z.B. über eine klare Target Framework Moniker (TFM)-Strategie (siehe Target Frameworks in NuGet), und in Node/TS über saubere Entry Points (siehe Node.js package exports).
Ein kurzes Beispiel: Das Interface ist öffentlich, die Implementierung bleibt ein Implementierungsdetail.
using System;
namespace Company.Product.Time;
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
internal sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
// src/index.ts
// Nur Exports sind Teil der öffentlichen API.
// Die .js-Endungen sind für ESM (ECMAScript Modules)-Runtime-Imports gedacht (z.B. mit NodeNext).
export { createClock } from './createClock.js';
export type { Clock } from './clock.js';
Naming & Scoping: Wie du Konflikte und Chaos vermeidest
Beim Naming geht es vor allem um Auffindbarkeit und Konfliktvermeidung. Nutzt in npm Scopes wie @company/* (z.B. @company/ui, @company/tsconfig) und bei NuGet ein konsistentes Prefix wie MyCompany.* (z.B. MyCompany.Product.Component).
Wenn ihr irgendwann öffentlich die Pakete bereitstellen wollt, lohnt sich bei NuGet ein früher Blick auf Package ID Prefix Reservation, um euer gewünschtes Prefix zu reservieren.
Behalte konsistente Namen und beachte die Regeln der Zielregistry. npm erlaubt z.B. keine Großbuchstaben, während NuGet PascalCase bevorzugt.
Wähle kurze klare Namen die Zweck und Verantwortung zeigen und vermeide Implementierungsdetails. Kennzeichne Team oder Produkt, damit Ownership und Audience klar sind (z.B. @company/<product>-sdk oder MyCompany.<Product>.Contracts).
Versioning & Release-Strategie (Semantic Versioning, Pre-Releases, Deprecation)
Semantic Versioning (SemVer) ist dein Kommunikationsvertrag, nicht nur ein Nummernschema. Wenn du Major/Minor/Patch sauber lebst, können Teams Abhängigkeiten gezielt updaten, ohne bei jedem Patch Angst vor Breaking Changes zu haben. Die Referenz ist die SemVer 2.0.0 Spezifikation, NuGet hat zusätzliche Details in NuGet Package Versioning.
Für kontrollierte Rollouts brauchst du Pre-Releases und eine klare Deprecation-Strategie (also: Versionen und APIs als veraltet markieren, statt sie abrupt zu entfernen). Pre-Releases helfen, neue Features oder Breaking Changes zu testen, bevor sie an die breite Masse gehen.
Meist ist es sinnvoll, Pre-Releases mit einem Label wie -preview oder -rc zu versehen, damit Konsumenten sie bewusst auswählen müssen.
In npm sind zusätzlich dist-tags ein sehr praktikables Werkzeug (siehe npm dist-tag), weil du damit Versionen zusätzlich mit Labels wie latest oder rc (Release Candidate) versiehst:
# Preview veröffentlichen und als rc markieren
npm publish --tag rc
# Später: stable auf latest
npm dist-tag add @company/sdk-users@2.0.0 latest
Der wichtigste Governance-Punkt hier ist Unveränderlichkeit: Veröffentlichte Versionen werden nicht überschrieben. Wenn du etwas fixen musst, erhöhst du die Version und veröffentlichst neu.
Wo veröffentlichen: Azure DevOps Artifacts, GitHub und öffentliche Registries
Interne Packages gehören in einen privaten Feed. Ein öffentliches Bereistellen passiert nur z.B. für Open Source oder ein gezielt unterstütztes öffentliches Software Development Kit (SDK).
Azure DevOps Artifacts ist stark, wenn du Enterprise-Workflows brauchst, insbesondere über Upstream Sources (Proxy/Cache) und Views für Promotion. GitHub Packages passt gut, wenn Berechtigungen und CI (Continuous Integration) ohnehin in GitHub liegen, die Einstiegsdokus sind npm in GitHub Packages und NuGet in GitHub Packages.
Denke bei Misch-Setups an Dependency Confusion (wenn ein öffentliches Package denselben Namen hat wie euer internes). Praktisch heißt das: interne Scopes, klares Registry-Routing und im Zweifel ein einziger Company Endpoint, der Public Packages proxied.
Für mehr Details zu eigenen Registries und Self-Hosting, siehe unseren Artikel zu Abhängigkeiten im Griff: Private Feeds für NuGet, npm, pip und Docker.
npm-Package erstellen (TypeScript) und publishen
Ein npm-Package steht und fällt mit Metadaten und Entry Points. Die Details zu package.json stehen in der package.json Referenz, hier ist ein fokussierter Ausschnitt für ein internes TS-Package:
{
"name": "@company/sdk-users",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./dist/index.js"
},
"types": "./dist/index.d.ts",
"files": ["dist"],
"license": "UNLICENSED"
}
Für die Registry-Zuordnung ist ein .npmrc der pragmatischste Start. Für GitHub Packages findest du das Setup in npm in GitHub Packages. Für Azure Artifacts ist das Vorgehen in npmrc usage in Azure Artifacts beschrieben.
# .npmrc
@company:registry=https://npm.pkg.github.com
always-auth=true
In CI (Continuous Integration) veröffentlichst du das Paket dann. Die Versionsnummer sollte manuell für Major/Minor-Änderungen erhöht werden. Die Patch-Nummer wird über die Pipeline automatisch erhöht, so dass man sich darum nicht kümmern muss. Das Erstellen des Pakets erfolgt über npm publish und das Ergebnis kann danach direkt in die Registry gepusht werden.
NuGet-Package erstellen (.NET) und publishen
In .NET ist Packaging sehr direkt über .csproj-Metadaten konfiguierbar. Einstieg: Creating a NuGet package und das Kommando dotnet pack.
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>Company.Product.UsersSdk</PackageId>
<Version>1.0.0</Version>
<Description>Internes SDK für die Users API</Description>
<RepositoryUrl>https://github.com/company/users-sdk</RepositoryUrl>
<PackageLicenseExpression>LicenseRef-Proprietary</PackageLicenseExpression>
</PropertyGroup>
Für CI gelten dieselben Prinzipien wie bei npm: Versionsnummer entsprechend vergeben, das Paket erstellen und dann in den Feed pushen. GitHub beschreibt das Pattern in Publishing .NET Packages. In Azure Pipelines ist Auth oft der erste Stolperstein, dafür ist die NuGetAuthenticate Task der Standard.
dotnet pack -c Release
# anschließend: dotnet nuget push ... (je nach Feed/Token)
Dein pragmatischer Startplan für interne Packages
Starte klein und standardisiere erst, wenn du die ersten echten Konsumenten hast.
Phase 1: Naming und Ownership festlegen, SemVer vereinbaren und eine minimale CI bauen, die auf Git-Tags veröffentlicht. Die wichtigste Regel ist hier schon Unveränderlichkeit.
Phase 2: Promotion und Pre-Releases einführen, plus Security-Basics wie automatische Updates und einfache Scans. Wenn du Supply Chain Security mit einer Software Bill of Materials (SBOM), also einer Stückliste deiner Komponenten, sauber in den Alltag bringen willst, schau in unseren Artikel: Governance: Software Bill of Materials (SBOM).
Phase 3: Templates bereitstellen (npm und .NET), damit neue Packages automatisch Docs, Metadaten und Release-Standards mitbringen. Ergänze dann Policies für Deprecation statt Löschung und halte Lizenz-Metadaten sauber (siehe npm license field und NuGet license metadata). Für Open Source sind SPDX-Expressions der Standard, siehe die SPDX License List.
Wie stellst du sicher, dass Shared Code bei euch nicht zu Copy-Paste und Versionschaos verkommt? Schau gerne bei uns auf LinkedIn vorbei und diskutiere mit.