Flexible Multi-Language Feedback Form System built with FastAPI, following Domain-Driven Design and Hexagonal Architecture principles.
- โ Multiple types of feedback forms (product feedback, support ticket, survey, custom)
- โ Multi-language support for forms and questions
- โ Multiple question types (text, rating, multiple choice)
- โ Full CRUD operations for forms (backoffice)
- โ Form retrieval and response submission (mobile/web apps)
- โ Response viewing (backoffice)
- โ Production-ready architecture with proper separation of concerns
- Python 3.11+
- Poetry (for dependency management)
make installmake runThe API will be available at http://localhost:8000
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
curl -X POST "http://localhost:8000/api/v1/backoffice/forms" \
-u admin:admin \
-H "Content-Type: application/json" \
-d '{
"type": "product_feedback",
"name": {
"en": "Product Feedback",
"es": "Feedback del Producto"
},
"description": {
"en": "Help us improve our product",
"es": "Ayรบdanos a mejorar nuestro producto"
},
"questions": [
{
"type": "rating",
"text": {
"en": "How satisfied are you?",
"es": "ยฟQuรฉ tan satisfecho estรกs?"
},
"required": true,
"min_rating": 1,
"max_rating": 5
},
{
"type": "text",
"text": {
"en": "Additional comments",
"es": "Comentarios adicionales"
},
"required": false
}
]
}'# Simple request
curl -X GET "http://localhost:8000/api/v1/mobile/forms/{form_id}"
# With campaign tags (for reference/tracking)
curl -X GET "http://localhost:8000/api/v1/mobile/forms/{form_id}?campaign=summer2024&source=email&group=premium_users"curl -X POST "http://localhost:8000/api/v1/mobile/responses?campaign=summer2024&source=email&group=premium" \
-H "Content-Type: application/json" \
-d '{
"form_id": "<form_id>",
"answers": [
{
"question_id": "<question_id>",
"value": 5
},
{
"question_id": "<question_id>",
"value": "Great product!"
}
],
"tags": {
"utm_source": "newsletter",
"utm_medium": "email"
}
}'Note: Tags from query parameters (campaign, source, group) are automatically merged with tags in the request body.
# Get all data for a user
curl -X GET "http://localhost:8000/api/v1/gdpr/data/user-123"
# Export user data as JSON file
curl -X GET "http://localhost:8000/api/v1/gdpr/data/user-123/export" -o user_data.json
# Delete all user data
curl -X DELETE "http://localhost:8000/api/v1/gdpr/data/user-123"make install- Install dependenciesmake test- Run unit testsmake test-integration- Run integration testsmake test-all- Run all testsmake lint- Run linters (Ruff)make type-check- Run type checking (mypy)make format- Format codemake clean- Clean artifactsmake run- Run the application
project/
โโโ domain/ # Domain layer (entities, value objects, repositories)
โโโ application/ # Application layer (use cases, DTOs, mappers)
โโโ infrastructure/ # Infrastructure layer (mock repositories, config)
โโโ presentation/ # Presentation layer (FastAPI routers)
โโโ tests/ # Tests organized by layers
The system follows Domain-Driven Design (DDD) with Hexagonal Architecture:
- Domain Layer: Pure business logic, independent of external concerns
- Application Layer: Use cases orchestrate domain operations
- Infrastructure Layer: Concrete implementations (currently mocked)
- Presentation Layer: FastAPI HTTP endpoints
The API is designed for two types of consumers:
- Base URL:
/api/v1/backoffice/ - Authentication: HTTP Basic Auth (username/password)
- Configure in
.env:BACKOFFICE_USERNAMEandBACKOFFICE_PASSWORD - Default:
admin/admin(change in production!)
- Configure in
- Endpoints:
POST /forms- Create form (requires auth)GET /forms- List all forms (requires auth)GET /forms/{form_id}- Get form details (requires auth)PUT /forms/{form_id}- Update form (requires auth)DELETE /forms/{form_id}- Delete form (requires auth)GET /responses- View responses (optional, requires auth)
- Base URL:
/api/v1/mobile/ - Endpoints:
GET /forms/{form_id}?campaign=X&source=Y&group=Z- Get form to display (tags optional, for reference)POST /responses?campaign=X&source=Y&group=Z- Submit form response (tags stored with response)
- Tags/Campaigns:
- Tags can be passed as query parameters:
campaign,source,group - Tags are stored with each response for tracking and analytics
- Example:
/api/v1/mobile/responses?campaign=summer2024&source=email&group=premium_users
- Tags can be passed as query parameters:
- Base URL:
/api/v1/gdpr/data/ - Endpoints:
GET /{user_id}- Get all user data (Right to Access - Art. 15 GDPR)GET /{user_id}/export- Export user data as JSON (Right to Portability - Art. 20 GDPR)DELETE /{user_id}- Delete all user data (Right to Erasure - Art. 17 GDPR)
- Usage: Users can access, export, or delete their personal data by providing their
user_id
Copy .env.example to .env and configure:
ENVIRONMENT=development
DEBUG=true
APP_NAME=feedback-form-system
API_SECRET_KEY=your-secret-key-minimum-32-charactersTests follow the GWT (Given-When-Then) format and are organized by layers.
make test # Unit tests
make test-integration # Integration tests
make test-all # All testsFor storing multilingual form/question data in the database:
- Current approach is appropriate - dictionaries are standard for stored multilingual data
- Libraries like
fastapi-babelare better suited for UI messages, error messages, and API documentation - If you need pluralization or complex formatting, consider
fastapi-babelfor error messages only - Keep
MultilingualTextfor domain data (forms, questions) as it's simple and flexible
Current Issue:
- Global variables in
dependencies.pyfor singleton pattern
Suggested Improvements:
- Use a more robust DI container (e.g.,
dependency-injector) - Or use FastAPI's
Depends()for automatic dependency injection
To migrate from mock repositories to a real database implementation:
-
Create new repository implementation in
infrastructure/persistence/:class PostgreSQLFormRepository(FormRepository): def __init__(self, db_connection): self._db = db_connection async def create(self, form: Form) -> Form: # PostgreSQL implementation ...
-
Update
infrastructure/config/dependencies.py:def get_form_repository() -> FormRepository: global _form_repository if _form_repository is None: # Change only this line: _form_repository = PostgreSQLFormRepository(get_db_connection()) # _form_repository = MockFormRepository() # โ Comment/remove return _form_repository
The architecture is designed to support this migration - only the repository implementations need to change, the domain and application layers remain unchanged.