Welcome to the Quiz Master Monorepo. This project is a Yarn workspace-based monorepo using Yarn v4.11.0 and Node.js 24. It follows a Microservices Architecture governed by Domain-Driven Design (DDD) principles and leverages Fastify for high-performance HTTP routing.
The system consists of an API Gateway and multiple internal microservices communicating synchronously via HTTP/JSON, and asynchronously via strictly typed Kafka events.
api-gateway: The single public-facing entry point, acting as a reverse proxy, aggregator, and global authenticator.ms-user: Manages user profiles, registration, and user-centric data.ms-quiz-management: Dedicated to managing quizzes, questions, and choices.ms-session: Manages live quiz sessions, participant tracking, and real-time state.ms-response: Records participant answers, calculates correctness, and tracks session progress.common/*: Shared libraries standardizing core behaviors like logging, authentication, database access, messaging, and error handling across all services.
The ms-quiz-management service provides:
- Full CRUD for Quizzes, Questions, and Choices.
- Bonus Operations:
getQuestionsByQuizId(quizId): Retrieve all questions belonging to a specific quiz, ordered by itsorder_index.getQuestionsByIds(ids[]): Bulk retrieve questions by id.getChoicesByQuestionId(questionId): Retrieve all choices for a specific question.
All services utilize the common-errors shared library to ensure consistent API responses:
BaseError: Standardized JSON structure for all application errors.- Domain-Specific Errors: Located in
./errorsfolders (e.g.,QUIZ_NOT_FOUND). - Postgres Integration: Services catch DB-specific errors (e.g., unique constraint violations) and map them to
ConflictError(409) orInternalServerError(500).
Our security model incorporates a unified authentication flow, relying on asymmetric cryptography (RSA Keys) to sign and verify tokens:
- Access Tokens & Refresh Tokens:
- The user authenticates against the gateway (which may delegate to
ms-user). - The gateway issues an Access Token (short-lived) and a Refresh Token (long-lived).
- The user authenticates against the gateway (which may delegate to
- Gateway Verification:
- The API Gateway uses
common-authto decode and verify the incoming purely on the Gateway layer using hooks (hookAccessToken/hookRefreshToken). - If invalid or missing on a protected route, the Gateway immediately responds with a
401 Unauthorizedwithout ever reaching the microservices.
- The API Gateway uses
- Internal Tokens:
- When the Gateway propagates a valid request to a Microservice, it generates an Internal Token (signed with its private key) via the
InternalTokenInterceptorprovided bycommon-auth. - The Microservice transparently intercepts the request, validates the Internal Token using
hookInternalToken, and reconstructs therequest.usercontext payload. - This ensures Zero-Trust within the cluster: Microservices only answer requests demonstrably originating from the authorized Gateway or legitimate Internal services.
- When the Gateway propagates a valid request to a Microservice, it generates an Internal Token (signed with its private key) via the
To decouple microservices and guarantee data consistency without distributed transactions, we use Apache Kafka for asynchronous communication. This ensures highly scalable, fault-tolerant event streaming across domains.
Microservices that dictate domain changes act as Producers.
For instance, when a new user registers in ms-user, it triggers a producer to publish a strictly typed event (e.g., USER_CREATED) to the user.events topic. We have enabled idempotence: true on the Kafka client itself to prevent network retries from polluting the topic.
Subscribing microservices (e.g., ms-quiz-management) act as Consumers that react to these domain topics. Due to the nature of distributed systems, at-least-once delivery is the standard. This means a consumer might receive the same message multiple times.
To protect against this, we implemented robust Consumer-Side Idempotency:
- Every message distributed via Kafka contains a strictly unique
eventId(UUID). - Before processing an event payload, the consumer queries a shared
processed_eventsPostgreSQL table (managed via TypeORM'sProcessedEventEntity). - If the
eventIdalready exists, the event is immediately discarded/skipped, preventing side-effect duplication. - If the
eventIdis new, the business logic runs and theeventIdis recorded in the same database transaction, ensuring atomic Exactly-Once semantics.
We rely on TypeORM automated migrations (yarn migrations:run) to ensure that the processed_events table exists and is synchronized across all domains that implement Kafka listeners.
The project integrates a complete observability stack for distributed tracing and log centralization via OpenTelemetry.
- OpenTelemetry (OTEL): Collects traces and logs from all microservices.
- Jaeger: Used for visualizing request paths through services (Tracing).
- OpenSearch: Search and analytics engine for centralized log storage.
- OpenSearch Dashboards: Visualization interface for logs (equivalent to Kibana).
- Jaeger UI: http://localhost:16686
- OpenSearch Dashboards: http://localhost:5601
- OpenSearch API: http://localhost:9200
To enable data exportation and start the full monitoring infrastructure:
MONITORING_ENABLED=true docker compose --profile app --profile monitoring up -dTip
Setting MONITORING_ENABLED=true tells the services to send their data to the OTEL collector. The monitoring profile starts the OpenSearch and Jaeger containers.
Shared libraries providing cross-cutting concerns.
common-auth: Fastify hooks & TS typings for token verification and internal token interception.common-core: Foundational classes (Controllers, Services), Decorators (e.g.,@Public(),@Schema()), and core helpers.common-crypto: Cryptographic utility wrappers and token signing/verifying functions.common-logger: Pino-based high-performance JSON logger.common-database: Standardized TypeORM / PostgreSQL configurations, migrations runner, and shared entities (e.g.,ProcessedEventEntity).common-kafka: Typed wrappers for Kafka consumer/producer with built-in idempotency logic and graceful disconnects.common-errors: Global custom error classes (e.g.,NotFoundError) and Fastify error handlers.common-axios: Pre-configured Axios instances for inter-service communication.common-swagger: OpenAPI generators utilizing Fastify-Swagger.common-config: Configuration validation relying on config/joi.common-monitoring: OpenTelemetry setup for tracing and OpenSearch integration for log centralization.
(See packages/common/README.md for more details on internal libraries)
- Purpose: Fastify-based orchestrator. Validates external JWTs and mints Internal Tokens before proxying API requests to
ms-*destinations. (Seepackages/api-gateway/README.mdfor full implementation details)
- Purpose: Domain-specific microservices encapsulating their own databases (PostgreSQL/Sequelize).
This project utilizes Yarn 4 via Corepack:
corepack enable
yarn set version 4.11.0Install all monorepo dependencies:
yarn installBefore running the services, you must initialize the PostgreSQL databases. We provide an init-db.sh script to automate this.
1. Start the infrastructure:
docker compose --profile infra up -d2. Run the initialization script: Depending on your environment, you may need to target different ports:
- Development Port (5433):
POSTGRES_PORT=5433 ./init-db.sh
- Test Port (5434):
POSTGRES_PORT=5434 ./init-db.sh
Important
The postgres container already executes this script on the first startup if the volume is empty. However, if you add new services or modify the script, you should run it manually as shown above.
If you are running the services locally for the first time or if Kafka topics have been lost, you must create them manually:
./scripts/setup-kafka.shThis will ensure all domain topics (like quizz.events) are created with the correct configurations.
With Docker Compose (Recommended): The easiest way to bootstrap the databases alongside the services:
docker compose --profile infra up -d
# Start services in docker
docker compose --profile app up -d
# Start services with full monitoring (OpenSearch + Tracing)
MONITORING_ENABLED=true docker compose --profile app --profile monitoring up -dNote: Make sure port 5432 is free on your host, or configure docker-compose.yml accordingly.
Manual Start (Development):
You can launch the applications simultaneously using concurrently:
yarn startOr individually:
yarn workspace @monorepo/api-gateway start
yarn workspace @monorepo/ms-user start
yarn workspace @monorepo/ms-quiz-management startWe use Vitest for our fast, native execution test suite, alongside ESLint and Prettier for code formatting.
-
Run all tests:
yarn test -
Run Unit Tests exclusively:
yarn test:unit
-
Run Integration Tests exclusively:
yarn test:int
-
Run End-to-End (E2E) Tests: E2E tests require a fully functional localized environment (databases, cache, microservices). We use a dedicated Docker Compose profile (
test) to spin up isolated containers before running the tests.# 1. Build and start the test environment (detached mode) docker compose --profile test up -d --build # 2. Run the E2E test script yarn test:e2e # 3. Tear down the test environment when finished docker compose --profile test down -v
-
Generate Coverage Report (Excludes config and useless files):
yarn vitest run --coverage
-
Run Linter:
yarn lint
- Database: PostgreSQL 16 (via TypeORM)
- Event Broker: Apache Kafka
- Routing: Fastify v5
- Testing: Vitest v4
- Language: Node 24 (ESM Modules, Mixed JS/TS)