Nx-Monorepo mit einer Angular-19-App und einer NestJS-10-API. Die User-Domäne wird zentral als Zod-Schema in libs/shared definiert und sowohl im Frontend (Reactive-Forms-Validator) als auch im Backend (Request-Pipe) ausgeführt — eine Quelle, zwei Konsumenten.
| Layer | Tech |
|---|---|
| Workspace | Nx 20 |
| Frontend | Angular 19 (Standalone), Material 3 |
| Backend | NestJS 10 (Express, async-mutex) |
| Validation | Zod 3, geteilt in libs/shared |
| Persistenz | JSON-Datei mit atomarem Write-Rename |
| Sprache | TypeScript 5.5 (strict) |
.
├── apps/
│ ├── api/ # NestJS REST API
│ │ ├── data/users.json # Persistenz (vom Seed erzeugt)
│ │ └── src/app/
│ │ ├── users/ # Controller / Service / Repository
│ │ └── common/ # ZodBody-Decorator
│ └── frontend/ # Angular App
│ └── src/app/
│ ├── users/ # Liste, Detail-Dialog, Create-Form
│ ├── smiley/ # Pure-CSS-Smiley (/smiley)
│ └── app.config.ts # Standalone-Bootstrap
├── libs/
│ └── shared/src/lib/
│ ├── user.schema.ts # Zod discriminatedUnion (admin/editor/viewer)
│ └── api-routes.ts # Geteilte Route-Konstanten
└── scripts/
└── seed-users.ts # Generiert 100 deterministische Test-User
npm install # 1× nach dem Klonen
npm run seed # Erzeugt apps/api/data/users.json (100 User)| Befehl | Zweck |
|---|---|
npm run start:api |
NestJS auf http://localhost:3000/api |
npm run start:frontend |
Angular auf http://localhost:4200 |
npm start |
Beide parallel via nx run-many -t serve |
npm run seed |
Seed neu generieren |
npm run build |
Production-Build beider Apps |
npm run lint |
ESLint über alle Projekte |
npm run test |
Jest-Tests |
Der Angular-Dev-Server proxyt /api/* automatisch auf http://localhost:3000 (siehe apps/frontend/proxy.conf.json).
| Endpoint | Body | Response |
|---|---|---|
GET /api/users |
— | User[] |
GET /api/users/:id |
— | User · 404 wenn unbekannt |
POST /api/users |
UserCreateInput |
User · 201 · 400 bei Validierungs-, 409 bei Email-Konflikt |
400-Antwort-Format (so geformt, dass das Frontend es 1:1 in den FormGroup-State setzen kann):
{
"statusCode": 400,
"message": "Validation failed",
"errors": {
"phoneNumber": "Telefonnummer ist erforderlich",
"birthDate": "Geburtsdatum ist erforderlich"
}
}Single package.json, geteilte tsconfig.base.json mit Path-Alias @pdr/shared, ein Lockfile, nx affected für selektives Bauen. NPM-Workspaces wären eine Alternative — Nx liefert zusätzlich nx graph, einheitliche Targets (build/test/lint/serve) und einen besseren DX-Loop.
Zod beschreibt Typ und Laufzeitprüfung in einer Quelle. z.infer<typeof userCreateSchema> ist exakt das, was die Pipe nach erfolgreicher Validierung herausgibt — kein doppeltes Pflegen von TS-Typen und Validierungs-Dekoratoren. Das Frontend importiert das gleiche Schema direkt; class-validator wäre durch DI/Reflect-Metadata zwar im Backend möglich, aber im Browser umständlich.
Die Validierung lebt nur dort. Frontend bindet sie als Cross-Field-Validator an die FormGroup, Backend als @ZodBody(schema)-Decorator. Eine Änderung in user.schema.ts propagiert atomar auf beiden Seiten — die "Frontend sagt OK / Backend gibt 400"-Bug-Klasse ist damit ausgeschlossen.
Drei explizite Branches (admin/editor/viewer) statt ein Schema + superRefine:
- TypeScript narrowed automatisch nach
role— keinif (role === 'admin') assertHas(phoneNumber)-Geknüpfel. - Fehler kommen ohne Extra-Pfad-Mapping aufs richtige Feld.
- Jede Branch ist selbstdokumentierend: "Admin braucht X+Y, Editor braucht X, Viewer nichts Extra."
- Mutex serialisiert Reads und Writes — der Node-Event-Loop kann keine Schreibvorgänge interleaven.
write tmp → renamegarantiert einen atomaren Wechsel des Storage-Files. Kein halb-geschriebenes JSON, selbst wenn der Prozess während des Schreibens stirbt.- Cross-Platform: OS-Level Filelocks (
flock) sind auf Windows notorisch fragil — ein In-Process-Mutex ist robuster und billiger. Bei horizontaler Skalierung würde dieser Layer komplett gegen eine echte DB getauscht.
Vollwertiges M3-Theme mit mat.theme() für komplette Tonal-Stufen, dann gezielter Override der Brand-Farben über --mat-sys-primary/-secondary/-tertiary/-error. Alle Material-Komponenten ziehen ihre Farben aus diesen CSS-Variablen — eine einzige Stelle für Brand-Anpassungen.
Standardisierter Pagination/Sort-Support mit eigenem filterPredicate für die Full-Name-Suche und eigenem sortingDataAccessor für die berechnete name-Spalte. Die @ViewChild-Setter für Paginator und Sort werden erst gebunden, wenn die Tabelle nach Laden der Daten ins DOM kommt (statt ngAfterViewInit als One-Shot).
Ein font-size: clamp(...) auf dem Wrapper steuert alle inneren em-Maße — perfekt responsive aus einer Knob. position: absolute wird nirgends verwendet, Pseudo-Elemente liefern Augen-Highlight und einen Hauch Tongue-Tint.
| Punkt | Stand | Ausbaupfad |
|---|---|---|
| Persistenz | JSON-Datei | PostgreSQL/Prisma, Migrationen |
| Auth | Keine — öffentliche Endpoints | JWT/OIDC, Role Guards |
| Pagination | Client-seitig (alle User auf einmal geladen) | Server-seitig mit ?page=&size= |
| Tests | Jest-Setup vorhanden, Specs noch nicht ausgebaut | Unit + e2e (Playwright) |
| Logging / Observability | NestJS Default-Logger | Pino + OTEL Exporter |
| Rate-Limit | — | @nestjs/throttler |
| Email-Verification | — | Confirmation-Token-Flow |
Die Architektur skaliert sauber in jede dieser Richtungen.
MIT