This is a personal study project modeled on concepts from Microsoft's sample e‑commerce stacks:
- dotnet/eShop — reference domain and bounded contexts
- .NET Aspire — ideas around local orchestration, observability hooks, and service-oriented composition
The implementation here is NestJS, React, Prisma, and Docker Compose, not .NET.
Independence. This codebase is not maintained by Microsoft or the dotnet/eShop authors. There is no guarantee that features, endpoints, databases, or UI match the upstream sample, nor that updates keep pace with Aspire or dotnet/eShop. Treat it as an educational fork in spirit only.
Storefront visuals only. To keep comparisons easy during study, the storefront layout echoes the reference app in a coarse way where practical, and reuses similar stock images/icons where files are bundled for that purpose. That is stylistic resemblance for learning—not an official port, remix, or endorsed UI.
This repository is for learning and local experimentation, not for advertising a hardened production deployment.
- Hybrid development (fast loop) —
pnpm infra:upstarts Postgres (pgvector), Redis, RabbitMQ, and the observability bundle (seedeploy/compose/observability/README.md). Usepnpm setup:localfor infra + DB migrations/seeds. Nest APIs, workers, and the mobile BFF run on the host withpnpm dev; Vite shells withpnpm dev:ui. - Packaged stack (
stackCompose profile) — Docker builds images for the same Nest services aspnpm devplus nginx for the storefront. Seedeploy/compose/README-stack.md.
.NET Aspire is not included. Docker Compose plus pnpm scripts provide repeatable topology on a laptop only — not feature parity with Aspire dashboards, resource graphs, or cloud deployment pipelines.
- Node.js 20.19+ or 22.12+ (see
package.jsonengines; required by Vite 8 for web apps) - pnpm 9.x (see
packageManagerinpackage.json) - Docker Desktop with Compose v2 (WSL2 filesystem recommended on Windows for bind mounts)
From the repository root:
cp .env.example .env
pnpm install
pnpm build
pnpm setup:local # infra + migrations + seeds (or: pnpm infra:up && pnpm db:setup)
pnpm dev # Nest APIs, workers, mobile-bffIn a second terminal for the web UIs (storefront, webhook tester, operations hub):
pnpm dev:uiCompose-only path (no host-run Nest): pnpm stack:up / pnpm stack:down — see [deploy/compose/README-stack.md](deploy/compose/README-stack.md).
Open Grafana (http://localhost:3200), Jaeger (http://localhost:16686), and Prometheus (http://localhost:9099) from the operations hub or directly. Stop infra with pnpm infra:down. Port list: deploy/compose/observability/README.md.
- Root
.env— copy from.env.example. Used bydotenv-clifordb:*anddevscripts. Never commit real secrets. - Compose —
deploy/compose/.env.compose.exampleforpnpm infra:up/pnpm infra:down(mergesobservability/docker-compose.observability.yml). For thestackprofile:deploy/compose/.env.stack.exampleanddeploy/compose/README-stack.md. Seedeploy/compose/README.mdfor other profiles. - Defaults in templates are for local dev only (for example Postgres
postgres/postgres, Redis and Rabbit on non-default host ports). Replace before any shared or public deployment.
Committed examples use weak secrets and optional auth bypass flags so exercises work without a formal threat model. Do not expose these stacks to the public internet unchanged.
| Variable | Role (study) | If leaked or left on a public host |
|---|---|---|
ESHOP_ORDERING_AUTH_BYPASS |
When 1 / true, Ordering HTTP may skip Bearer validation |
Unauthenticated access to ordering APIs |
ESHOP_ORDERING_JWT_SECRET |
Symmetric material Ordering trusts when bypass is off; stack identity-service receives the same value via ESHOP_JWT_SYMMETRIC_SECRET interpolation so issued tokens can validate downstream when you turn bypass off |
Token forgery |
ESHOP_JWT_SYMMETRIC_SECRET (identity code path) |
Signs JWTs in identity-service (compose sets it from the ordering secret for alignment) |
Forged identity tokens |
ESHOP_BASKET_JWT_SECRET |
Basket HTTP/gRPC demo validation | Session / basket abuse |
ESHOP_WEBHOOKS_AUTH_BYPASS |
When 1, Webhooks API skips strict JWT for local demos |
Misuse of webhook registration endpoints |
See .env.example, deploy/compose/.env.stack.example, and SECURITY.md. Treat every secret in templates as disposable lab material.
| Goal | Use pnpm script |
Manual alternative |
|---|---|---|
| Install workspace | pnpm install |
— |
| Start infra + observability | pnpm infra:up |
docker compose -f deploy/compose/docker-compose.yml -f deploy/compose/observability/docker-compose.observability.yml --env-file deploy/compose/.env.compose.example up -d --wait |
| Migrations + seeds | pnpm db:setup (after cp .env.example .env) |
Per-service pnpm --filter @eshop/<service> prisma:migrate |
| Nest APIs on host | pnpm dev |
Start each app with pnpm --filter @eshop/<service> start:dev |
| Vite UIs | pnpm dev:ui |
Per-app pnpm --filter @eshop/storefront-web dev, etc. |
| Full stack in Docker | pnpm stack:up / pnpm stack:down |
Same compose files as documented in deploy/compose/README-stack.md |
| Kubernetes (study) | make -C deploy/k3d k3d-up then helm-install-dev |
helm lint deploy/helm/eshop-nestjs; see [deploy/k3d/README.md](deploy/k3d/README.md) |
| GitOps (study) | — | kubectl apply per [deploy/argocd/README.md](deploy/argocd/README.md) |
| OIDC E2E (optional) | pnpm test:e2e:oidc |
Keycloak Compose + JWKS on APIs — [deploy/keycloak/README.md](deploy/keycloak/README.md) |
| OpenAPI snapshots (maintainers / CI) | pnpm contracts:export-openapi (APIs must be running) |
GET http://127.0.0.1:<port>/api/docs-json per service |
Fresh clone minimum: cp .env.example .env → pnpm install → pnpm build → pnpm dev (plus pnpm dev:ui in a second terminal). Run pnpm test:unit when you want a quick sanity check. Everything else (infra, contracts:*, E2E, k3d, Keycloak) is optional.
With pnpm dev running, open Swagger UI per service (JSON at /api/docs-json):
| Service | Port | Swagger UI |
|---|---|---|
| identity-service | 5051 | http://127.0.0.1:5051/api/docs |
| catalog-service | 5052 | http://127.0.0.1:5052/api/docs |
| ordering-service | 5053 | http://127.0.0.1:5053/api/docs |
| basket-service | 5054 | http://127.0.0.1:5054/api/docs |
| webhooks-service | 5055 | http://127.0.0.1:5055/api/docs |
| mobile-bff | 5070 | http://127.0.0.1:5070/api/docs (static gateway contract) |
Workers (order-grace-worker, payment-worker) expose health HTTP only — no public REST catalogue. When you change HTTP contracts on purpose, regenerate snapshots with pnpm contracts:export-openapi (with pnpm dev running) and commit the JSON under contracts/openapi/nest/. CI runs pnpm contracts:check; you do not need it to run the stack locally. See contracts/openapi/README.md.
| Step | Command |
|---|---|
| Lint + format (Biome) | pnpm lint or pnpm format:check |
| Lint + compile (quick gate) | pnpm check |
| PR CI parity (build + contracts + tests) | pnpm ci (Docker for integration/E2E) |
| Compile all packages and apps | pnpm build |
| Unit and selected contract tests | pnpm test:unit |
| OpenAPI snapshot gate (CI; optional locally) | pnpm contracts:check |
| Playwright smoke (no stack) | pnpm test:smoke |
| Integration tests (Docker / Testcontainers) | pnpm test:integration |
| Playwright E2E (probes; skips if stack down) | pnpm test:e2e |
| Helm chart lint (requires Helm CLI) | helm lint deploy/helm/eshop-nestjs |
GitHub Actions (.github/workflows/eshop-nest-ci.yml) runs install, pnpm build, contract checks, and tests. Locally, after pnpm install: pnpm ci (Docker required for integration/E2E) or pnpm check for lint + compile only
| Component | Location / notes |
|---|---|
| Nest microservices | identity, catalog, ordering, basket, webhooks |
| Workers | order-grace-worker, payment-worker |
| mobile-bff | HTTP proxy on 5070 |
| Frontends | storefront-web, webhook-client, operations-dashboard (Vite) |
| PostgreSQL | Compose initdb → logical DBs per context |
| Redis | Basket projections |
| RabbitMQ | Integration events + DLQ |
| Keycloak | Optional Compose IdP — deploy/keycloak/ |
| Observability | Grafana, Jaeger, Prometheus, Loki, OTel collector — deploy/compose/observability/ |
| Helm | deploy/helm/eshop-nestjs (8 workloads + migrate Jobs + PDB/HPA/ServiceMonitor) |
| Argo CD | GitOps manifests — deploy/argocd/, deploy/gitops/environments/ |
| k3d (optional) | Local K3s rehearsal — deploy/k3d/ |
Not in this repository: Temporal, Kong, Istio, Vault, Apicurio, .NET Aspire AppHost, MAUI/HybridApp.
| Area | Choices |
|---|---|
| Language / runtime | TypeScript on Node 20+ |
| API framework | NestJS (HTTP; basket-service gRPC) |
| Web apps | React, Vite, Tailwind; @eshop/ui |
| Data | Prisma → PostgreSQL; Redis (basket) |
| Messaging | RabbitMQ; outbox (@eshop/outbox); inbox (@eshop/inbox); @eshop/event-bus-amqp |
| Auth | Symmetric JWT (identity-service) + optional Keycloak OIDC (@eshop/auth JWKS) |
| Observability | @eshop/observability, Pino, Compose observability merge |
| Testing | Vitest/Jest unit; Playwright tests/e2e; Testcontainers tests/integration |
The upstream solution under eShop-main/src/ is shaped roughly as follows:
- One ASP.NET Core project per service host:
Basket.API,Catalog.API,Ordering.API,Identity.API,Webhooks.API, plus background hostsOrderProcessorandPaymentProcessor. - Ordering vertical slice:
Ordering.Domain,Ordering.Infrastructure,Ordering.API— domain model and EF persistence separated from HTTP. Ordering.API/Application: CQRS-style commands/queries, validators, pipeline behaviors, and integration-event emission.IntegrationEventLogEFandEventBus/EventBusRabbitMQ: transactional outbox and messaging abstractions.eShop.ServiceDefaults: shared OpenTelemetry and health conventions for Aspire.eShop.AppHost: Aspire orchestrator (dashboard, resource bindings, debugging).- Frontends:
WebApp(Blazor),WebAppComponents,WebhookClient; mobile-orientedClientApp/HybridApp. **tests/**: unit and functional suites per bounded context.
This is documented in the upstream eShop-main/README.md architecture diagram (img/eshop_architecture.png), not reproduced here.
Repository shape follows pnpm workspaces (pnpm-workspace.yaml): runnable code in apps/, reusable libraries in packages/, and cross-service artefacts in contracts/.
Inside each Nest microservice (for example apps/ordering-service/src/), layering is intentionally consistent:
| Folder (typical) | Role |
|---|---|
api/ |
HTTP controllers (and module wiring), DTOs, guards, Swagger/OpenAPI |
application/ |
Use cases: command/query handlers, orchestration workflows, domain-facing facades |
integration/ |
AMQP subscribers, integration-event payloads, outbox publish queries |
infrastructure/ |
Prisma modules, adapters to PostgreSQL |
Workers (order-grace-worker, payment-worker) expose small HTTP health endpoints and concentrate on Rabbit-driven logic.
packages/domains/ordering holds framework-free aggregates and rules (same idea as Ordering.Domain, without EF).
packages/infrastructure/* replaces cross-cutting .NET DLLs (EventBus*, telemetry helpers, filters) as small typed packages.
Patterns reused in spirit
- Domain-driven boundaries — separate Postgres databases per context; catalog vs ordering vs webhooks vs identity.
- Outbox + message bus — write business data and outbox rows in one transaction; background publisher delivers to RabbitMQ (same conceptual role as
IntegrationEventLogEF+ bus). - CQRS-lite — explicit command/query style handlers in Nest
application/layers rather than fat controllers everywhere. - Backend-for-frontend —
mobile-bffproxies catalog, ordering, basket, and identity HTTP. In the Composestackprofile, identity proxy is on by default (port 5070). The storefront uses 5051 for login andVITE_ESHOP_BFF_ORIGINfor the server-backed cart when authenticated. - API versioning and OpenAPI — Nest services expose Swagger UI at
/api/docs; snapshots live undercontracts/openapi/.
Design system (UI)
- Reference: Blazor
WebApp+WebAppComponentsfrom dotnet eShop. - Here: Tailwind CSS tokens via
@eshop/ui(shadcn-style React primitives) reused bystorefront-webandwebhook-client.
Operational UX
- Reference: Aspire dashboard bundles logs, traces, URLs.
- Here:
operations-dashboardSPA polls health URLs and deep-links Grafana / Prometheus / Jaeger when observability is up (pnpm infra:up).
This section explains how this repo echoes the reference stacks and where it diverges by design or necessity.
| Topic | Reference world (dotnet/eShop, Aspire) | This repo |
|---|---|---|
| Orchestration | Aspire AppHost wires projects, connection strings, and dashboards | Docker Compose profiles plus root pnpm scripts. No Aspire host: you start infra and apps explicitly. |
| Service discovery | Aspire resource references and generated wiring | Explicit URLs in ESHOP_* / VITE_* variables. Easier for Node but more manual than AppHost codegen. |
| Observability story | Aspire promotes OTEL exporters and telemetry integration | Compose merge (observability/) started with pnpm infra:up, plus @eshop/observability in processes. |
| API shape | ASP.NET controllers, YARP gateway in the sample | Nest controllers, mobile-bff as a thin HTTP proxy for mobile-style clients; storefront calls catalog/ordering directly from the browser in dev (Vite env). |
| UI | Blazor-based sample WebApp | React + Vite SPA. Similar page flow and assets only where useful for comparison—not a faithful Blazor replica. |
| Data stack | EF Core, SQL Server variants in tutorials | Prisma + PostgreSQL (pgvector for catalog AI-related features when enabled). Different ORM and engine. |
| Integration events | MassTransit-oriented patterns in the .NET ecosystem | RabbitMQ + outbox tables; routing keys and flows documented under contracts/. Event names often follow the reference vocabulary for easier mental mapping; wire formats are documented in-repo. |
What stayed intentionally similar
- Bounded contexts (catalog, ordering, basket, identity, webhooks, background payment/grace concepts).
- Integration-event vocabulary for order lifecycle and catalog signals (see
contracts/integration-events.md). - gRPC contract for basket (
contracts/grpc/basket.proto). - A BFF-shaped service (
mobile-bff) for consolidation patterns; upstream dotnet gateway tech differs.
What did not stay similar (and why)
- Aspire UI and project graph — not applicable to a Node monorepo without building a parallel tool; Compose + scripts are the pragmatic substitute.
- Byte-for-byte UI parity — React and Tailwind differ from Blazor; only partial layout and assets align.
- Compose
stackprofile — same Nest footprint aspnpm devplus storefront nginx.webhook-clientandoperations-dashboardstill expectpnpm dev:uion the host unless you add more images. - Full production hardening — samples use fake credentials; this repo matches that learning posture (see
SECURITY.md).
Abbreviated tree (omit node_modules, dist, Prisma generated trees, lockfiles). Each Nest service usually mirrors src/api, src/application, src/integration, src/infrastructure, plus prisma/schema.prisma.
eShopOnContainers-NestJS/
├── apps/
│ ├── basket-service/ # Redis + RabbitMQ; HTTP health + gRPC basket API
│ ├── catalog-service/ # Postgres (catalogdb) + RabbitMQ AI hooks optional
│ ├── identity-service/ # Postgres (identitydb)
│ ├── mobile-bff/ # Reverse proxy aggregation for mobile-style callers
│ ├── operations-dashboard/ # SPA: health probes + Grafana/Prometheus/Jaeger links
│ ├── order-grace-worker/
│ ├── ordering-service/ # Postgres (orderingdb) + RabbitMQ
│ ├── payment-worker/
│ ├── storefront-web/ # Main React storefront (Vite)
│ ├── webhook-client/ # SPA to exercise Webhooks API
│ └── webhooks-service/ # Postgres (webhooksdb) + RabbitMQ
├── contracts/
│ ├── golden/ # Integration-event payload samples (JSON)
│ ├── grpc/ # basket.proto
│ ├── openapi/nest/ # Published OpenAPI snapshots
│ └── *.md # Integration-event catalogue docs
├── deploy/
│ ├── compose/ # docker-compose*.yml, env templates, initdb/, observability/
│ ├── docker/ # Dockerfiles + nginx storefront config
│ ├── helm/eshop-nestjs/ # Reference Kubernetes chart
│ ├── k3d/ # Optional local K3s lifecycle (Makefile)
│ ├── argocd/ # Argo CD Application manifests (GitOps study)
│ ├── gitops/environments/ # Per-env Helm value overlays
│ └── keycloak/ # Optional Keycloak Compose + realm export
├── packages/
│ ├── domains/ # basket, catalog, identity, ordering
│ └── infrastructure/ # auth, event-bus-amqp, health, http-resilience,
│ # idempotency, inbox, integration-event-types,
│ # observability, openapi-common, outbox,
│ # shared-exception-filters, ui
├── tests/
│ ├── e2e/ # `@eshop/e2e-tests` Playwright
│ └── integration/ # `@eshop/integration-tests` Vitest + containers
├── .env.example
├── package.json
└── pnpm-workspace.yaml
| Path | Responsibility |
|---|---|
apps/* |
Runnable Nest servers, React/Vite shells, Dockerfile build targets |
packages/domains/* |
Domain aggregates and validation without framework imports |
packages/infrastructure/* |
Messaging, telemetry, guards, filters, HTTP helpers, @eshop/ui |
contracts/ |
gRPC/proto, golden JSON fixtures, integration-event catalogue, exported OpenAPI |
deploy/compose/ |
Local infra stacks, Observability merges, Postgres init scripts |
deploy/docker/ |
Multi-stage Nest + storefront images |
deploy/helm/ |
Kubernetes chart (study) |
deploy/k3d/ |
Optional k3d cluster + Helm (Makefile targets) |
deploy/argocd/ |
GitOps bootstrap manifests |
tests/e2e |
Browser automation |
tests/integration |
Backing-service integration coverage |
Modern browser clients call backends over HTTP. mobile-bff targets callers that want one host port (5070) proxying catalog, ordering, basket, and (in the Compose stack profile) identity. The storefront calls identity/catalog/ordering via VITE_* origins (5051–5053) and syncs the cart through the BFF (VITE_ESHOP_BFF_ORIGIN → /api/basket) when logged in; guests keep sessionStorage unless VITE_ESHOP_CART_GUEST_MODE=true.
Operations dashboard traffic is telemetry and health probes (/api/health, /alive, /health depending on app) plus links to Grafana; it never participates in catalogue or order workflows.
flowchart TB
subgraph browser["Browser clients"]
SF[storefront-web]
WHCli[webhook-client]
OPS[operations-dashboard]
end
subgraph apis["Application tier"]
ID[identity-service]
CAT[catalog-service]
ORD[ordering-service]
BSK[basket-service]
WH[webhooks-service]
BFF[mobile-bff]
GRC[order-grace-worker]
PAY[payment-worker]
end
subgraph data_plane["Stateful dependencies"]
PG[(PostgreSQL\nidentitydb catalogdb orderingdb webhooksdb)]
RD[(Redis\nbasket slots)]
RMQ[(RabbitMQ\nnot used inline in UX requests)]
end
SF -->|"REST JWT login/register"| ID
SF -->|"catalog REST"| CAT
SF -->|"orders REST"| ORD
WHCli -->|"webhook API"| WH
OPS -.->|"GET health probes only"| ID
OPS -.-> CAT
OPS -.-> ORD
OPS -.-> BSK
OPS -.-> WH
OPS -.-> BFF
OPS -.-> GRC
OPS -.-> PAY
BFF -->|"HTTP proxy"| CAT
BFF -->|"HTTP proxy"| ORD
BFF -->|"/identity proxy (stack default)"| ID
ID --> PG
CAT --> PG
ORD --> PG
WH --> PG
BSK --> RD
CAT -.->|"async publishes"| RMQ
ORD -.->|"async publishes"| RMQ
BSK -.->|"async publishes"| RMQ
WH -.->|"async publishes"| RMQ
Services treat business commands as transactional work in PostgreSQL and enqueue integration-event rows (outbox). A dispatcher reads those rows and publishes to RabbitMQ with routing keys from contracts/integration-events.md. Separate consumers (possibly the same codebase, different subscriptions) mutate local state.
The diagram emphasizes principal message paths rather than listing every correlation id or retry policy.
flowchart LR
subgraph producers["Transactional publishers outbox to AMQP"]
ORD_pub[ordering-service]
CAT_pub[catalog-service]
PAY_pub[payment-worker]
GRACE_pub[order-grace-worker]
end
BUS(("RabbitMQ"))
subgraph subscribers["Consumers"]
ORD_sub[ordering-service]
CAT_sub[catalog-service]
BSK_sub[basket-service]
WH_sub[webhooks-service]
PAY_sub[payment-worker]
end
ORD_pub -->|order lifecycle events| BUS
CAT_pub -->|stock and price events| BUS
PAY_pub -->|payment outcomes| BUS
GRACE_pub -->|GracePeriodConfirmed| BUS
BUS -->|OrderStarted| BSK_sub
BUS -->|AwaitingValidation| CAT_sub
BUS -->|stock confirmed or rejected| ORD_sub
BUS -->|stock confirmed saga| PAY_sub
BUS -->|payment succeeded or failed| ORD_sub
BUS -->|paid or shipped hooks| CAT_sub
BUS -->|paid shipped price| WH_sub
BUS -->|GracePeriodConfirmed| ORD_sub
How to read it
- Left column emits events after Postgres commits—the outbox guarantees AMQP publishes track database state (
@eshop/outbox). - Center remains the shared broker topology (queues/bindings derive from
@eshop/event-bus-amqpsetup). - Right column reacts: for example
basket-serviceclears state onOrderStartedIntegrationEvent;catalog-serviceparticipates in awaiting-validation and paid flows;payment-workerandordering-servicecontinue the saga after stock confirmation.
Consult contracts/integration-events.md for the publisher/consumer table and golden/ for JSON samples.
flowchart LR
dev[Developer laptop]
k3d[k3d cluster eshop-dev]
helm[Helm chart eshop-nestjs]
pods[Nest Deployments + Services]
dev -->|make -C deploy/k3d k3d-up| k3d
dev -->|helm-install-dev| helm
helm --> pods
k3d --> pods
Compose + pnpm dev remain the default loop; k3d exercises probes, Services, and Helm values without a cloud cluster. Details: [deploy/k3d/README.md](deploy/k3d/README.md).
Postgres (host 55432), Redis (56379), RabbitMQ AMQP (55672, management 55673). Application HTTP defaults include catalog 5052, ordering 5053, basket 5054, webhooks 5055, identity 5051, workers 5065 / 5066, mobile BFF 5070. Vite UIs: storefront 5173, webhook client 5174, operations hub 5188 (if a port is busy, Vite picks the next free one). Loki 3100, Grafana 3200, Prometheus 9099, Jaeger 16686 when pnpm infra:up includes observability. Static stack storefront on 8080 with the stack profile.
| Topic | Location |
|---|---|
| Compose profiles and env | deploy/compose/README.md |
| Full stack in Docker | deploy/compose/README-stack.md |
| Helm chart | deploy/helm/eshop-nestjs/README.md |
| k3d local cluster | deploy/k3d/README.md |
| Argo CD GitOps | deploy/argocd/README.md |
| Keycloak OIDC | deploy/keycloak/README.md |
| Observability merge | deploy/compose/observability/README.md |
| E2E tests | tests/e2e/README.md |
| Contract artefacts | contracts/README.md |
| Security expectations | SECURITY.md |
MIT License. Third-party notices: NOTICE.