A Flask RESTful API for managing restaurant menus. This service allows users to create, manage, and organize their restaurant menus with detailed dish attributes including taste profiles, textures, emotions, and visual characteristics.
- 🔐 JWT-based authentication
- 📋 Full CRUD operations for menus and dishes
- 🎨 Rich dish attributes (taste, texture, color, emotions)
- 👤 User isolation (users can only access their own data)
- 🛡️ Rate limiting protection
- 📊 Admin dashboard with analytics
- 🐳 Docker support
- Python 3.10+
- Poetry (recommended) or pip
# Clone the repository
git clone https://github.com/YOUR_USERNAME/menu-server-demo.git
cd menu-server-demo
# Install dependencies
poetry install
# Initialize the database
poetry run flask --app wsgi db upgrade
# Run the development server
poetry run flask --app wsgi run --debug# Clone the repository
git clone https://github.com/YOUR_USERNAME/menu-server-demo.git
cd menu-server-demo
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Initialize the database
flask --app wsgi db upgrade
# Run the development server
flask --app wsgi run --debug# Create your .env file from example
cp .env.example .env
# Edit .env with your settings (especially SECRET_KEY and ADMIN_PASSWORD)
# Production with SQLite (default)
docker compose up -d
# Production with PostgreSQL
docker compose --profile postgres up -d
# Production with MariaDB
docker compose --profile mariadb up -d
# Development (with hot-reload)
docker compose --profile dev up
# Build manually
docker build -t soundfood-api --target production .Note: On first startup, the database is automatically initialized with:
- All required tables
- Default attributes (emotions, textures, shapes)
- An admin user (credentials from
.envor defaults: admin/admin123)
Create a .env file from the example:
cp .env.example .env| Variable | Description | Default |
|---|---|---|
SECRET_KEY |
Flask secret key | dev-secret-key |
JWT_SECRET_KEY |
JWT signing key | dev-jwt-secret |
DATABASE_URL |
Database connection string | sqlite:///instance/project.db |
FLASK_ENV |
Environment (development/production) |
development |
ADMIN_USERNAME |
Default admin username | admin |
ADMIN_PASSWORD |
Default admin password | admin123 |
ADMIN_EMAIL |
Default admin email | admin@example.com |
AUTO_INIT_DB |
Auto-initialize database on startup | true |
The application supports multiple database backends:
SQLite (default - good for development and small deployments):
DATABASE_URL=sqlite:///instance/project.dbPostgreSQL (recommended for production):
DATABASE_URL=postgresql://user:password@localhost:5432/soundfoodMariaDB/MySQL:
DATABASE_URL=mysql+pymysql://user:password@localhost:3306/soundfoodThe API uses Flask-Caching to improve performance. By default, it uses in-memory caching (SimpleCache).
| Endpoint | Cache Duration | Description |
|---|---|---|
GET /api/emotions |
7 days | Emotion attributes (static data) |
GET /api/textures |
7 days | Texture attributes (static data) |
GET /api/shapes |
7 days | Shape attributes (static data) |
For production with multiple workers, consider using Redis:
# In your configuration
CACHE_TYPE = "RedisCache"
CACHE_REDIS_URL = "redis://localhost:6379/0"All API endpoints (except authentication and health check) require a valid JWT token in the Authorization header:
Authorization: Bearer <your_token>
GET /api/healthResponse (200 - Healthy):
{
"status": "healthy",
"database": "connected"
}Response (503 - Unhealthy):
{
"status": "unhealthy",
"database": "disconnected"
}POST /auth/register
Content-Type: application/json
{
"username": "string",
"password": "string"
}Response (201):
{
"message": "User created successfully",
"user_id": 1
}POST /auth/login
Content-Type: application/json
{
"username": "string",
"password": "string"
}Response (200):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"user_id": 1
}POST /auth/logout
Authorization: Bearer <token>Response (200):
{
"message": "Successfully logged out"
}GET /auth/me
Authorization: Bearer <token>Response (200):
{
"id": 1,
"username": "john_doe",
"role": "user",
"created_at": "2026-01-03T10:30:00+00:00",
"updated_at": "2026-01-03T12:45:00+00:00"
}PATCH /auth/me/email
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "newemail@example.com"
}Response (200):
{
"message": "Email updated successfully"
}Error Responses:
400- Invalid email format409- Email already in use
PATCH /auth/me/password
Authorization: Bearer <token>
Content-Type: application/json
{
"current_password": "current_password123",
"new_password": "new_password456"
}Response (200):
{
"message": "Password updated successfully"
}Error Responses:
400- Password validation failed (minimum 8 characters, must contain letters and digits)401- Current password is incorrect
DELETE /auth/me
Authorization: Bearer <token>
Content-Type: application/json
{
"password": "your_password123"
}Response (200):
{
"message": "Account deleted successfully"
}Note: Deleting an account will permanently remove all associated menus and dishes.
Error Responses:
401- Password is incorrect
GET /api/menus
Authorization: Bearer <token>Response (200):
[
{
"id": 1,
"title": "Lunch Menu",
"description": "Daily lunch specials",
"status": "draft",
"dish_count": 5,
"created_at": "2026-01-03T10:30:00+00:00",
"updated_at": "2026-01-03T12:45:00+00:00"
}
]GET /api/menus/{menu_id}
Authorization: Bearer <token>Response (200):
{
"id": 1,
"title": "Lunch Menu",
"description": "Daily lunch specials",
"status": "draft",
"created_at": "2026-01-03T10:30:00+00:00",
"updated_at": "2026-01-03T12:45:00+00:00",
"dishes": [...]
}POST /api/menus
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Dinner Menu",
"description": "Evening fine dining"
}Response (201):
{
"message": "Menu created",
"id": 2
}PUT /api/menus/{menu_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Updated Title",
"description": "Updated description",
"status": "submitted"
}Note: Status must be either draft or submitted.
Response (200):
{
"message": "Menu updated"
}Error Responses:
400- Invalid status value403- Unauthorized to modify this menu404- Menu not found
POST /api/menus/{menu_id}/submit
Authorization: Bearer <token>Changes the menu status from draft to submitted.
Response (200):
{
"message": "Menu submitted successfully"
}Error Responses:
400- Menu is already submitted403- Unauthorized to submit this menu404- Menu not found
DELETE /api/menus/{menu_id}
Authorization: Bearer <token>Response (200):
{
"message": "Menu deleted"
}GET /api/menus/{menu_id}/dishes
Authorization: Bearer <token>Response (200):
[
{
"id": 1,
"name": "Spaghetti Carbonara",
"description": "Classic Roman pasta",
"section": "Primi",
"emotions": [{"id": 1, "description": "Comfort"}],
"textures": [{"id": 1, "description": "Creamy"}],
"shapes": [{"id": 1, "description": "Long"}],
"bitter": 0,
"salty": 3,
"sour": 1,
"sweet": 0,
"umami": 5,
"fat": 4,
"piquant": 1,
"temperature": 4,
"colors": ["#F5DEB3", "#FFD700"],
"created_at": "2026-01-03T10:30:00+00:00",
"updated_at": "2026-01-03T12:45:00+00:00"
}
]POST /api/menus/{menu_id}/dishes
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Margherita Pizza",
"description": "Traditional Neapolitan pizza",
"section": "Pizze",
"bitter": 0,
"salty": 2,
"sour": 2,
"sweet": 1,
"umami": 4,
"fat": 3,
"piquant": 0,
"temperature": 5,
"color1": "#FF6347",
"color2": "#FFFFFF",
"color3": "#228B22",
"emotion_ids": [1, 2],
"texture_ids": [3],
"shape_ids": [2]
}Response (201):
{
"message": "Dish created",
"id": 5
}PUT /api/dishes/{dish_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Updated Dish Name",
"salty": 4
}Response (200):
{
"message": "Dish updated"
}DELETE /api/dishes/{dish_id}
Authorization: Bearer <token>Response (200):
{
"message": "Dish deleted"
}GET /api/emotions
Authorization: Bearer <token>Response (200):
[
{"id": 1, "description": "Happy"},
{"id": 2, "description": "Nostalgic"}
]GET /api/textures
Authorization: Bearer <token>Response (200):
[
{"id": 1, "description": "Crunchy"},
{"id": 2, "description": "Smooth"}
]GET /api/shapes
Authorization: Bearer <token>Response (200):
[
{"id": 1, "description": "Round"},
{"id": 2, "description": "Square"}
]All endpoints return consistent error responses:
{
"error": "Error message description"
}| Status Code | Description |
|---|---|
| 400 | Bad Request - Invalid input data |
| 401 | Unauthorized - Missing or invalid token |
| 403 | Forbidden - Access denied to resource |
| 404 | Not Found - Resource doesn't exist |
| 409 | Conflict - Resource already exists |
| 429 | Too Many Requests - Rate limit exceeded |
The application includes an admin dashboard accessible at /admin.
- Users must have
is_admin=Trueoris_manager=Trueto access the dashboard - Admins have full CRUD access to all models
- Managers have read-only access to logs and analytics
Use the Flask CLI to create an admin user:
poetry run flask --app wsgi shellfrom app.models import User
admin = User.create(username="admin", password="secure_password", is_admin=True)- User management
- Menu and dish overview
- Request logs and analytics
- Daily API usage charts
# Run all tests
poetry run pytest
# Run with verbose output
poetry run pytest -v
# Run with coverage report
poetry run pytest --cov=app --cov-report=html
# Run specific test file
poetry run pytest tests/test_auth.pyThis project uses Ruff for linting and formatting.
# Check for linting issues
poetry run ruff check .
# Fix linting issues automatically
poetry run ruff check --fix .
# Format code
poetry run ruff format .
# Check formatting without making changes
poetry run ruff format --check .# Create a new migration
poetry run flask --app wsgi db migrate -m "Description of changes"
# Apply migrations
poetry run flask --app wsgi db upgrade
# Rollback last migration
poetry run flask --app wsgi db downgrademenu-server-demo/
├── app/
│ ├── __init__.py # Application factory
│ ├── config.py # Configuration classes
│ ├── extensions.py # Flask extensions
│ ├── middleware.py # Request logging middleware
│ ├── cli.py # CLI commands
│ ├── api/
│ │ └── __init__.py # API endpoints
│ ├── auth/
│ │ └── __init__.py # Authentication endpoints
│ ├── admin/
│ │ ├── __init__.py
│ │ ├── routes.py # Admin auth routes
│ │ └── views.py # Flask-Admin views
│ └── models/
│ ├── __init__.py
│ ├── user.py
│ ├── menu.py
│ ├── attributes.py
│ └── request_log.py
├── templates/
│ └── admin/ # Admin dashboard templates
├── migrations/ # Database migrations
├── tests/ # Test suite
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md
The API implements rate limiting to prevent abuse:
| Endpoint | Limit |
|---|---|
| Register | 5/minute |
| Login | 10/minute |
| Update Email | 10/minute |
| Update Password | 10/minute |
| Delete Account | 5/minute |
| GET endpoints | 60/minute |
| POST endpoints | 20-30/minute |
| DELETE endpoints | 10-20/minute |
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests and linting (
poetry run pytest && poetry run ruff check .) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request