Skip to content

MissingDataDev/pdr-challenge

Repository files navigation

User Management

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.

Stack

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)

Projektstruktur

.
├── 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

Setup

npm install        # 1× nach dem Klonen
npm run seed       # Erzeugt apps/api/data/users.json (100 User)

Starten

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).


API

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"
  }
}

Architektur-Entscheidungen

1. Nx als Monorepo

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.

2. Zod statt class-validator

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.

3. Schema in libs/shared (Single Source of Truth)

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.

4. Conditional Validation via z.discriminatedUnion

Drei explizite Branches (admin/editor/viewer) statt ein Schema + superRefine:

  • TypeScript narrowed automatisch nach role — kein if (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."

5. Persistenz: In-Memory + async-mutex + atomic rename

  • Mutex serialisiert Reads und Writes — der Node-Event-Loop kann keine Schreibvorgänge interleaven.
  • write tmp → rename garantiert 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.

6. Material 3 Theme mit System-Token-Override

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.

7. UsersListComponent — MatTableDataSource + Custom Predicate

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).

8. Smiley — Pure Flexbox

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.


Bekannte Einschränkungen

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.


Lizenz

MIT

About

Probeaufgabe PDR

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors