A TypeScript-based Node.js prototype for a patient intake platform, built with clean architecture principles, event-driven design, and modern best practices.
This project follows Clean Architecture principles with clear separation of concerns:
src/
βββ domain/ # Business logic and entities
β βββ entities/ # Domain models (FormEntity, BaseEntity)
β βββ events/ # Domain events (IntakeCompletedEvent)
β βββ repositories/ # Repository interfaces
βββ application/ # Application services and use cases
β βββ services/ # Business logic services (FormService)
βββ infrastructure/ # External concerns and implementations
βββ controllers/ # HTTP controllers (FormController)
βββ repositories/ # Repository implementations (FormMongoRepository)
βββ database/ # Database connections (MongoConnection)
βββ messaging/ # Message broker (RabbitMQConnection, EventDispatcher, EventConsumer)
βββ dispatchers/ # Event dispatchers (IntakeCompletedDispatcher)
βββ consumers/ # Event consumers (IntakeCompletedConsumer)
- RESTful API for patient intake form management
- Event-Driven Architecture using RabbitMQ for asynchronous processing
- MongoDB for data persistence
- Type-Safe with TypeScript
- Clean Architecture with dependency inversion
- Docker Support with docker-compose
- Health Check Endpoint for monitoring
- Comprehensive Testing (unit and integration tests)
- Node.js >= 24.13.0
- MongoDB 7+
- RabbitMQ 3.12+
- Docker & Docker Compose (optional)
# Install dependencies
yarn install
# Build the project
yarn build
# Start the server
yarn start# Start all services (MongoDB, RabbitMQ, API)
docker-compose up -d
# View logs
docker-compose logs -f api
# Stop services
docker-compose downCheck the health status of the application and its dependencies.
Endpoint: GET /health
Response:
{
"status": "ok",
"timestamp": "2026-01-22T10:30:00.000Z",
"checks": {
"database": {
"connected": true,
"ping": true,
"status": "healthy"
},
"rabbitmq": {
"connected": true,
"status": "healthy"
}
}
}Retrieve all patient intake forms.
Endpoint: GET /forms
Response:
[
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2026-01-22T10:00:00.000Z",
"updatedAt": "2026-01-22T10:00:00.000Z",
"fields": {
"firstName": {
"id": "firstName",
"label": "First Name",
"type": "text"
},
"dob": {
"id": "dob",
"label": "Date of Birth",
"type": "date"
}
}
}
]Retrieve a specific form by its ID.
Endpoint: GET /forms/:id
Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2026-01-22T10:00:00.000Z",
"updatedAt": "2026-01-22T10:00:00.000Z",
"fields": {
"firstName": {
"id": "firstName",
"label": "First Name",
"type": "text"
}
}
}Error Responses:
404 Not Found- Form not found400 Bad Request- Invalid form ID
Create a new patient intake form.
Endpoint: POST /forms
Request Body:
{
"fields": {
"firstName": {
"id": "firstName",
"label": "First Name",
"type": "text"
},
"age": {
"id": "age",
"label": "Age",
"type": "number"
},
"medicalHistory": {
"id": "medicalHistory",
"label": "Medical History",
"type": "textarea"
}
}
}Field Types:
text- Single-line text inputnumber- Numeric inputdate- Date pickercheckbox- Checkbox inputradio- Radio buttonselect- Dropdown selecttextarea- Multi-line text input
Response: 201 Created
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2026-01-22T10:00:00.000Z",
"updatedAt": "2026-01-22T10:00:00.000Z",
"fields": { ... }
}Update an existing form.
Endpoint: PUT /forms/:id
Request Body:
{
"fields": {
"firstName": {
"id": "firstName",
"label": "First Name (Updated)",
"type": "text"
}
}
}Response: 200 OK
Error Responses:
404 Not Found- Form not found400 Bad Request- Invalid request body
Delete a form by ID.
Endpoint: DELETE /forms/:id
Response: 204 No Content
Error Responses:
404 Not Found- Form not found
Represents a patient intake form with customizable fields.
Properties:
id: string- Unique identifier (UUID)createdAt: Date- Creation timestampupdatedAt: Date- Last update timestampfields: FormFields- Dynamic form fields
Methods:
FormEntity.create(params)- Create a new form instanceFormEntity.from(params)- Recreate a form from existing data
Type: Record<string, FormField>
A collection of form fields with the following structure:
interface FormField {
id: string;
label: string;
type: "text" | "number" | "date" | "checkbox" | "radio" | "select" | "textarea";
}The application uses RabbitMQ for asynchronous event processing with a topic exchange pattern.
Published when a patient intake form is completed.
Event Payload:
{
eventId: string;
eventType: "IntakeCompleted";
aggregateId: string;
occurredOn: Date;
payload: {
formId: string;
completedBy: Date;
answers: Record<string, unknown>;
}
}Routing Key: form.intake.completed
Class: IntakeCompletedDispatcher
Methods:
Initialize the dispatcher and assert the exchange.
Dispatch a single intake completed event.
Parameters:
aggregateId- The aggregate/form IDpayload- Event payload with formId, completedBy, and answers
Returns: true if successfully published
Dispatch multiple events in a batch.
Parameters:
events- Array of events to dispatch
Returns: Array of boolean results
Class: EventConsumer
Methods:
Register a handler for a specific event type.
Example:
consumer.on("IntakeCompletedEvent", async (event) => {
console.log("Processing intake:", event);
// Handle the event
});Initialize the consumer, create queue, and bind to exchange.
Start consuming messages from the queue.
Stop consuming and close the channel.
Dispatcher Config:
{
exchange: "intake_events",
exchangeType: "topic",
durable: true
}Consumer Config:
{
queueName: "intake_completed_queue",
exchange: "intake_events",
routingKeys: ["form.*"],
prefetchCount: 10,
durable: true
}Application service for form business logic.
Methods:
Create a new form.
Retrieve a form by ID.
Retrieve all forms.
Update an existing form.
Delete a form by ID.
MongoDB implementation of the FormRepository interface.
Methods:
Save or update a form entity.
Find a form by ID.
Find all forms.
Delete a form by ID.
Singleton for managing MongoDB connections.
Methods:
Establish a connection to MongoDB.
Close the MongoDB connection.
Get the database instance.
Check if the database is responsive.
Properties:
connected: boolean- Connection status
# Run all tests
yarn test
# Run with coverage
yarn test --coverage
# Run specific test file
yarn test FormService.test.tsTest Types:
- Unit Tests - Business logic and services
- Integration Tests - Repository, database, and messaging
- E2E Tests - Full workflow testing
Singleton for managing RabbitMQ connections.
Methods:
Get the singleton instance.
Establish a connection to RabbitMQ.
Get a shared channel for publishing.
Create a new dedicated channel.
Close the connection.
Check connection status.
# Server Configuration
PORT=3000
# MongoDB Configuration
MONGODB_URI=mongodb://localhost:27017
MONGODB_DB_NAME=node-prototype
# RabbitMQ Configuration
RABBITMQ_URI=amqp://admin:password@localhost:5672
# Environment
NODE_ENV=development# Development
yarn dev # Watch mode with auto-reload
# Production
yarn build # Compile TypeScript
yarn start # Start production server
# Code Quality
yarn lint # Run ESLint
yarn format # Format code with Prettier
# Testing
yarn test # Run testsThe docker-compose.yml includes:
-
MongoDB (Port 27017)
- Version: 7
- Credentials: admin/password
- Health checks enabled
-
RabbitMQ (Ports 5672, 15672)
- Version: 3.12 with management
- Management UI: http://localhost:15672
- Credentials: admin/password
-
API (Port 3000)
- Auto-restarts on failure
- Health checks enabled
- Waits for dependencies
To run the event consumer separately:
node dist/infrastructure/consumers/IntakeCompletedConsumer.js- Dependency Inversion - Domain layer has no dependencies on infrastructure
- Single Responsibility - Each class has one reason to change
- Open/Closed - Open for extension, closed for modification
- Interface Segregation - Repository interfaces define clear contracts
- Event-Driven - Loose coupling via domain events
- Type Safety - Full TypeScript coverage
- Follow the existing code structure
- Write tests for new features
- Run linting and formatting before commits
- Use conventional commit messages
MIT License - see LICENSE file for details
Matheus Frizo matheusfrizo@gmail.com