TypeScript Projektaufbau – kompakt, modern, ohne Framework
Wenn du ein schlankes TypeScript-Projekt starten willst (CLI-Tool, kleiner Service, Skript), brauchst du meist keine riesige Toolchain. Mit TypeScript + esbuild (Build) in einem ESM-Projekt bekommst du:
- schnelle Builds
- saubere Ordnerstruktur
- modernes
import/export - einfache
npm run build/npm run dev
- TypeScript Projektaufbau – kompakt, modern, ohne Framework
- Ziel: eine sinnvolle Grundstruktur
- Voraussetzungen
- Schritt 1: Projekt initialisieren
- Schritt 2: `package.json` für ESM + Scripts
- Schritt 3: `tsconfig.json` (Editor + Typencheck)
- Schritt 4: esbuild Build (Config-Datei)
- Schritt 5: Beispielcode
- Build & Run
- Typische Stolperfallen (ESM + TypeScript)
- Varianten: App vs. Library
- Fazit
Hinweis
Dieser Artikel geht von Node.js als Runtime aus und nutzt ESM (ECMAScript Modules).
ESM ist dabei keine „Toolchain-Komponente“, sondern die Art, wie dein JavaScript-Projekt (Imports/Exports) aufgebaut ist.
Das ist heute der „moderne“ Standard, hat aber ein paar Regeln (Dateiendungen/type: module) – dazu gleich mehr.
Ziel: eine sinnvolle Grundstruktur
Eine gute Standard-Struktur für kleine bis mittlere Projekte (mit klarer Trennung in frontend, common und backend):
1 | my-ts-app/ |
src/: dein Quellcode (TypeScript)src/backend/: Node-Code (z. B. CLI/Service)src/frontend/: Browser-Code (UI)src/common/: gemeinsam genutzte Module
dist/: Build-Ausgabe (JavaScript)tsconfig.json: TypeScript-Compiler-Settings (für Typencheck & Editor)esbuild: übernimmt den Build (schnell) und kann bundlen oder „nur“ transpilen
Voraussetzungen
- Node.js (aktuelles LTS empfohlen)
- npm (oder pnpm/yarn – die Beispiele nutzen npm)
Schritt 1: Projekt initialisieren
1 | mkdir my-ts-app |
Optional, aber praktisch:
1 | npx tsc --init |
(Wir passen tsconfig.json gleich an.)
Schritt 2: `package.json` für ESM + Scripts
Wichtig für ESM in Node: setze "type": "module".
Beispiel package.json (relevanter Teil):
1 | { |
Warum diese Aufteilung?
typecheck: TypeScript macht nur Typprüfung (kein Output)build: esbuild baut nachdist/dev: esbuild im Watch-Modusstart: startet das gebaute JavaScript
Schritt 3: `tsconfig.json` (Editor + Typencheck)
Für esbuild ist tsconfig.json primär für Typprüfung und Editor-Features relevant.
Wichtig: esbuild macht keine Type-Checks. Es transpiliert TypeScript zu JavaScript und kann dabei auch Code ausgeben, den der TypeScript-Compiler (tsc) aus Typ-Gründen ablehnen würde.
Darum ist npm run typecheck (mit tsc --noEmit) ein guter Standard.
Eine solide Basis:
1 | { |
Hinweise:
- Wenn du wirklich nur Backend (Node) baust, kannst du
"DOM"inlibwieder entfernen. moduleResolution: "Bundler"brauchst du fürs Bauen mit esbuild in der Regel nicht.- Mit
NodeNextorientierst du dich stärker an den Regeln der Node-ESM-Runtime.
Schritt 4: esbuild Build (Config-Datei)
Lege eine Datei esbuild.config.mjs im Projektroot an:
1 | import esbuild from 'esbuild'; |
Was hier passiert:
platform: 'node': esbuild optimiert für Nodeformat: 'esm': Ausgabe bleibt ESM (import/export)bundle: true: (hier fürs Frontend) alles wird zu einem Bundle zusammengefasstsourcemap: true: Debugging indist/ist angenehm
Wenn du (z. B. im Frontend) nicht bundlen willst, setze bundle: false und achte stärker auf ESM-Imports/Dateiendungen.
Schritt 5: Beispielcode
Lege diese Dateien an:
src/common/logger.ts
1 | export function logInfo(message: string): void { |
src/backend/index.ts
1 | import { logInfo } from '../common/logger.js'; |
src/frontend/index.ts
1 | import { logInfo } from '../common/logger.js'; |
Wichtig
Siehst du das .js in ../common/logger.js?
Bei ESM in Node ist das häufig nötig, weil Node zur Laufzeit JavaScript-Dateien lädt.
TypeScript akzeptiert diese Schreibweise trotzdem, wenn die Settings passen (siehe moduleResolution).
Build & Run
1 | npm run typecheck |
Wenn du nur eins davon bauen willst:
1 | npm run build -- --backend |
Für Entwicklung (Watch):
1 | npm run dev |
Typische Stolperfallen (ESM + TypeScript)
- Fehlendes
"type": "module": Dann interpretiert Node Ausgaben ggf. als CommonJS. - Import-Pfade ohne
.js: Bei ESM in Node kann das knallen. Im Frontend (Bundle) fällt das oft weniger auf – im Backend ohne Bundle musst du es sauber machen. require/__dirname: In ESM gibt’srequirenicht direkt. Du nutzt stattdessenimport.meta.url+fileURLToPath.
Minimal-Beispiel für __dirname in ESM:
1 | const __filename = import.meta.filename; |
Varianten: App vs. Library
- Backend-App/CLI/Tool: oft
bundle: false(näher an Node-Runtime, weniger „Magie“). Wenn du ein Single-File-Deployment willst, kannst du trotzdem bundlen. - Frontend: praktisch immer
bundle: true. - Library: eher
bundle: false, dafür sauberer Export-Plan (z. B.exports-Map inpackage.json) und ggf. zusätzlich CJS-Output.
Fazit
Mit dieser Struktur hast du ein modernes TypeScript-Projekt, das schnell baut, sauber typgecheckt wird und ESM nutzt – ohne Overkill.
Wenn du willst, ergänze ich dir als nächsten Schritt auch noch:
exports-Map für Library-Releases- Dual-Publish (ESM + CJS)
- Minimal-Setup für Tests (z. B. Vitest)