This is a simple Todo List API built with FastAPI and Python 3.13+. This project is currently being used for Python full-stack candidates.
- FastAPI - Modern, fast web framework for building APIs
- Python 3.13+ - Latest Python with full type hints
- Pydantic v2 - Data validation using Python type annotations
- In-memory storage - Simple data persistence (resets on restart)
- Poetry - Modern dependency management
- pytest - Comprehensive unit tests with mocking
- Ruff - Extremely fast Python linter and formatter
- mypy - Static type checker with strict mode
- DevContainer - VS Code development container support
- Python 3.13+ or Docker with VS Code DevContainer support
- Poetry (if running locally without Docker)
# Install Poetry if you haven't already
curl -sSL https://install.python-poetry.org | python3 -
# Install dependencies
poetry install
# Activate virtual environment
poetry shell- Open the project in VS Code
- Install the "Dev Containers" extension
- Press
F1and select "Dev Containers: Reopen in Container" - Dependencies will be installed automatically
# Using Poetry
poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Or if inside poetry shell
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000The API will be available at:
- API: http://localhost:8000
- Interactive API docs (Swagger): http://localhost:8000/docs
- Alternative API docs (ReDoc): http://localhost:8000/redoc
All endpoints are prefixed with /api/todolists:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/todolists |
Get all todo lists |
| GET | /api/todolists/{id} |
Get a specific todo list |
| POST | /api/todolists |
Create a new todo list |
| PUT | /api/todolists/{id} |
Update an existing todo list |
| DELETE | /api/todolists/{id} |
Delete a todo list |
# Run all tests
poetry run pytest
# Run tests with coverage
poetry run pytest --cov=app --cov-report=html
# Run tests in watch mode
poetry run pytest-watch
# Run tests with verbose output
poetry run pytest -v# Check for linting errors
poetry run ruff check .
# Fix linting errors automatically
poetry run ruff check --fix .
# Format code
poetry run ruff format .# Run type checker
poetry run mypy app/
# Run type checker on all files
poetry run mypy ..
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI application entry point
│ ├── models.py # Pydantic models (schemas)
│ ├── routers/
│ │ ├── __init__.py
│ │ └── todo_lists.py # TodoList API endpoints
│ └── services/
│ ├── __init__.py
│ └── todo_lists.py # Business logic and in-memory storage
├── tests/
│ ├── __init__.py
│ └── test_todo_lists.py # Unit tests for all endpoints
├── .devcontainer/ # VS Code DevContainer configuration
├── pyproject.toml # Poetry dependencies and tool configs
├── .ruff.toml # Ruff linter/formatter configuration
├── mypy.ini # mypy type checker configuration
└── README.md
This project uses modern Python development tools:
- Poetry: Dependency management and packaging
- Ruff: Extremely fast linter and formatter (replaces Black, isort, flake8)
- mypy: Static type checker with strict mode enabled
- pytest: Testing framework with async support
- httpx: HTTP client for testing FastAPI endpoints
This application uses in-memory storage (Python lists/dicts). Data will be lost when the application restarts. This is intentional for simplicity and is suitable for interview/demo purposes.
Check integration tests at: https://github.com/crunchloop/interview-tests
- Martín Fernández (mfernandez@crunchloop.io)
We strongly believe in giving back 🚀. Let's work together Get in touch.
This is a FastAPI-based backend for a TodoList application that uses Redis as both a database and task queue system. The application provides a REST API for managing todo lists and items, with asynchronous task processing capabilities for bulk operations.
- FastAPI - Modern, fast web framework for building APIs
- Python 3.13 - Programming language
- Redis - In-memory data structure store (used as database and message broker)
- RQ (Redis Queue) - Simple Python library for queueing jobs
- Poetry - Dependency management and packaging
- Pydantic - Data validation using Python type annotations
- Pytest - Testing framework
- Full CRUD operations for Todo Lists
- Full CRUD operations for Todo Items
- Asynchronous task processing with Redis Queue
- Background workers for long-running operations
- Job status tracking
- Duplicate name/title validation
- Bulk operations (complete all items)
- CORS enabled for frontend integration
- Comprehensive test suite
Before running this project, ensure you have:
- Docker and Docker Compose
- Python 3.13 (if running locally without Docker)
- Poetry (for dependency management)
- Redis (included in Docker setup)
This project uses Redis in two distinct ways:
- Stores all TodoLists and TodoItems as JSON
- Maintains auto-incrementing ID counters
- Provides fast key-value storage
- Manages asynchronous job queue using RQ
- Handles background task processing
- Enables non-blocking operations for expensive tasks
Why Redis?
- Fast in-memory operations
- Simple key-value storage for small-scale applications
- Built-in pub/sub for job queuing
- Easy to scale and deploy
This project includes a complete dev container setup for consistent development environments.
Dev Container Configuration (.devcontainer/devcontainer.json):
{
"name": "FastAPI Todo App",
"dockerComposeFile": [
"../docker-compose.yml",
"docker-compose.yml"
],
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/devcontainers/features/python:1": {
"version": "3.13",
"installTools": true
},
"ghcr.io/devcontainers/features/git:1": {
"version": "latest"
}
},
"forwardPorts": [8000],
"postCreateCommand": "pip install poetry && poetry install",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"ms-python.mypy-type-checker"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.formatting.provider": "none",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
},
"ruff.enable": true,
"mypy-type-checker.importStrategy": "fromEnvironment"
}
}
}
}Steps to run with Docker:
- Open the project in VS Code
- Install the "Dev Containers" extension
- Press
F1and select "Dev Containers: Reopen in Container" - Wait for the container to build and dependencies to install
- The
postCreateCommandwill automatically runpoetry install
- Install Poetry:
pip install poetry- Install dependencies:
poetry install- Ensure Redis is running locally on port 6379
version: '3.8'
services:
app:
image: mcr.microsoft.com/devcontainers/python:1-3.13-bookworm
volumes:
- .:/workspaces/python-interview
ports:
- "8000:8000"
command: sleep infinity
working_dir: /workspaces/python-interview
networks:
- fastapi-network
networks:
fastapi-network:
driver: bridgeversion: '3.8'
services:
app:
environment:
- REDIS_URL=redis://redis:6379
depends_on:
- redis
volumes:
- ..:/workspaces:cached
command: sleep infinity
redis:
image: redis:7-alpine
ports:
- "6379:6379"Important: The dev container setup includes Redis automatically. The app service depends on Redis and connects via host.docker.internal:6379.
app/
├── routers/ # API route handlers
│ ├── __init__.py
│ ├── jobs.py # Job status endpoints
│ ├── todo_items.py # Todo items CRUD endpoints
│ └── todo_lists.py # Todo lists CRUD endpoints
├── services/ # Business logic layer
│ ├── __init__.py
│ ├── todo_items.py # Todo items service
│ └── todo_lists.py # Todo lists service (Redis storage)
├── __init__.py
├── main.py # FastAPI application entry point
├── models.py # Pydantic models for validation
├── redis_config.py # Redis connection and queue setup
└── worker.py # Background worker for async tasks
scripts/
└── start_worker.py # Helper script to start worker
tests/
├── __init__.py
├── test_todo_items.py # Tests for todo items
└── test_todo_lists.py # Tests for todo lists
Configuration Files:
├── docker-compose.yml # Docker services configuration
├── poetry.toml # Poetry configuration
├── poetry.lock # Locked dependencies
├── pyproject.toml # Project metadata and dependencies
├── .ruff.toml # Ruff linter configuration
├── mypy.ini # MyPy type checker configuration
├── .gitignore # Git ignore rules
└── README.md # Project documentationpoetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Expected output:
vscode ➜ /workspaces/python-interview (main) $ poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 INFO: Will watch for changes in these directories: ['/workspaces/python-interview'] INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Started reloader process [2940] using WatchFiles INFO: Started server process [3069] INFO: Waiting for application startup. INFO: Application startup complete.
The API will be available at http://localhost:8000
Important: The worker must be running for asynchronous operations (like "complete all") to work.
poetry run python -m app.workerExpected output:
vscode ➜ /workspaces/python-interview (main) $ poetry run python -m app.worker Worker started. Connecting to Redis... Worker ready. Waiting for jobs... 15:51:27 Worker e6e648a08bf84a8e9b851cba34ef64aa: started with PID 3371, version 2.6.1 15:51:27 Worker e6e648a08bf84a8e9b851cba34ef64aa: subscribing to channel rq:pubsub:e6e648a08bf84a8e9b851cba34ef64aa 15:51:27 *** Listening on default... 15:51:27 Worker e6e648a08bf84a8e9b851cba34ef64aa: cleaning registries for queue: default
FastAPI provides automatic interactive API documentation:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
GET /
Returns a simple message indicating the API is running.
GET /api/todolists # Get all todo lists
GET /api/todolists/{id} # Get a specific todo list
POST /api/todolists # Create a new todo list
PUT /api/todolists/{id} # Update a todo list
DELETE /api/todolists/{id} # Delete a todo listGET /api/todolists/{list_id}/items # Get all items in a list
GET /api/todolists/{list_id}/items/{item_id} # Get a specific item
POST /api/todolists/{list_id}/items # Create a new item
PUT /api/todolists/{list_id}/items/{item_id} # Update an item
PATCH /api/todolists/{list_id}/items/{item_id}/toggle # Toggle item completion
DELETE /api/todolists/{list_id}/items/{item_id} # Delete an item
POST /api/todolists/{list_id}/items/complete-all # Complete all items (async)GET /api/jobs/{job_id} # Check status of an async job
The "complete all" operation is implemented as an asynchronous task to handle large lists efficiently:
Client → POST /api/todolists/{id}/items/complete-all ↓ Enqueue job to Redis Queue ↓ Return job_id immediately (202 Accepted) ↓ Background Worker processes job ↓ Update all items in Redis DB
Router (routers/todo_items.py):
@router.post("/complete-all", status_code=202)
async def complete_all_async(todo_list_id: int) -> dict:
"""Queues the task and responds immediately."""
job_id = enqueue_complete_all(todo_list_id)
return {
"message": "queued job",
"job_id": job_id,
"todo_list_id": todo_list_id,
"check_status": f"/api/jobs/{job_id}",
}Redis Configuration (redis_config.py):
# Redis connection for queue (DB 0)
redis_conn = redis.Redis(host="host.docker.internal", port=6379, db=0)
queue = Queue(connection=redis_conn)
def enqueue_complete_all(todo_list_id: int) -> str:
"""Enqueues the job and returns job ID."""
from app.worker import complete_all_task
job = queue.enqueue(complete_all_task, todo_list_id)
return job.idWorker (worker.py):
def complete_all_task(todo_list_id: int) -> dict:
"""Task that the worker executes."""
service = get_todo_item_service()
todo_list_service = get_todo_list_service()
todo_list = todo_list_service.get(todo_list_id)
if not todo_list:
return {"error": "List not found"}
completed_count = 0
for i, item in enumerate(todo_list.items):
if not item.completed:
todo_list.items[i] = item.__class__(
id=item.id,
title=item.title,
description=item.description,
completed=True,
)
completed_count += 1
if completed_count > 0:
todo_list_service.save(todo_list)
return {
"completed": completed_count,
"message": f"Completed {completed_count} tasks"
}After receiving a job_id, clients can check the status:
GET /api/jobs/{job_id}Response:
{
"id": "abc123",
"status": "finished",
"result": {
"completed": 5,
"message": "Completed 5 tasks"
},
"error": null
}Job Statuses:
queued- Job is waiting to be processedstarted- Job is currently being processedfinished- Job completed successfullyfailed- Job failed with an error
- Non-blocking: API responds immediately, doesn't wait for task completion
- Scalability: Can handle large lists without timeout issues
- Better UX: Frontend can show progress or poll for status
- Resource efficiency: Worker can process jobs in background
- Error handling: Failed jobs can be retried or logged
class TodoList(BaseModel):
id: int
name: str
items: list[TodoItem] = []class TodoItem(BaseModel):
id: int
title: str
description: Optional[str] = None
completed: bool = Falseclass TodoListCreate(BaseModel):
name: str # min_length=1
class TodoListUpdate(BaseModel):
name: str # min_length=1
class TodoItemCreate(BaseModel):
title: str # min_length=1
description: Optional[str] = None
completed: bool = False
class TodoItemUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = NoneFeatures:
- Duplicate name validation (case-insensitive)
- Auto-incrementing IDs using Redis counters
- JSON serialization/deserialization for Redis storage
- Cascade operations (items included with lists)
Redis Keys:
todolist:{id}- Stores serialized TodoListtodolist:next_id- Global counter for list IDs
Features:
- Duplicate title validation within each list (case-insensitive)
- Per-list ID generation
- Toggle completion status
- Bulk complete operation
Redis Keys:
todoitem:{list_id}:next_id- Counter for items in specific list- Items are stored within their parent TodoList
The project includes a comprehensive test suite using pytest.
Run all tests:
poetry run pytestRun specific test file:
poetry run pytest tests/test_todo_lists.py -v
poetry run pytest tests/test_todo_items.py -vRun tests with coverage:
poetry run pytest --cov=app --cov-report=htmlRun tests in watch mode:
poetry run pytest-watchRun tests with verbose output:
poetry run pytest -vThe test suite includes comprehensive unit tests using mocked services:
TodoList Tests (test_todo_lists.py):
- GET /api/todolists - Returns all todo lists
- GET /api/todolists - Returns empty list when no todos exist
- GET /api/todolists/{id} - Returns a specific todo list by ID
- GET /api/todolists/{id} - Returns 404 when todo list not found
- POST /api/todolists - Creates new todo list successfully
- POST /api/todolists - Validates required fields (422 error)
- POST /api/todolists - Validates name is not empty (422 error)
- PUT /api/todolists/{id} - Updates existing todo list
- PUT /api/todolists/{id} - Returns 404 when todo list not found
- PUT /api/todolists/{id} - Validates required fields (422 error)
- DELETE /api/todolists/{id} - Deletes existing todo list (204 response)
- DELETE /api/todolists/{id} - Returns 404 when todo list not found
TodoItem Tests (test_todo_items.py):
- GET /api/todolists/{id}/items - Returns all items from a list
- GET /api/todolists/{id}/items/{item_id} - Returns a specific item
- GET /api/todolists/{id}/items/{item_id} - Returns 404 when item not found
- POST /api/todolists/{id}/items - Creates new item successfully
- POST /api/todolists/{id}/items - Rejects duplicate titles (400 error)
- PUT /api/todolists/{id}/items/{item_id} - Updates existing item
- PUT /api/todolists/{id}/items/{item_id} - Rejects duplicate title of another item (400 error)
- PATCH /api/todolists/{id}/items/{item_id}/toggle - Toggles completion status
- DELETE /api/todolists/{id}/items/{item_id} - Deletes an item (204 response)
- POST /api/todolists/{id}/items/complete-all - Enqueues async job (202 response)
Testing Strategy:
- Uses
pytestfixtures for test client and mocked services - Mocks
TodoListServiceandTodoItemServiceto isolate endpoint logic - Tests HTTP status codes, response structure, and service method calls
- Covers success cases, validation errors, and not found scenarios
- Validates FastAPI's automatic request validation (422 errors)
# Redis for data storage (DB 1)
redis_conn = Redis(
host="host.docker.internal", # Docker host
port=6379,
db=1, # Database 1 for data
decode_responses=True # Automatic string decoding
)
# Redis for job queue (DB 0)
redis_conn = Redis(
host="host.docker.internal",
port=6379,
db=0 # Database 0 for queue
)Why host.docker.internal?
- Allows containers to connect to services on the host machine
- Redis runs in a separate container, accessible via Docker networking
- Alternative: Use service name
redisif both services are in same compose file
- DB 0: Job queue and worker communication
- DB 1: Application data (TodoLists and TodoItems)
This separation prevents queue operations from interfering with data storage.
The API is configured to accept requests from any origin:
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Production Note: Restrict allow_origins to specific domains in production.
- Ruff: Fast Python linter and formatter
- MyPy: Static type checker
- Pytest: Testing framework
The dev container automatically installs:
- Python extension
- Pylance (language server)
- Ruff (linting/formatting)
- MyPy type checker
# Add a new dependency
poetry add package-name
# Add a dev dependency
poetry add --group dev package-name
# Update dependencies
poetry update
# Show installed packages
poetry show
# Run a command in the virtual environment
poetry run python script.pyProblem: ConnectionError: Error 111 connecting to redis:6379
Solutions:
- Ensure Redis container is running:
docker ps - Check Redis is accessible:
redis-cli ping - Verify
host.docker.internalresolves correctly - Check firewall settings
Problem: Jobs stay in "queued" status
Solutions:
- Ensure worker is running:
poetry run python -m app.worker - Check worker logs for errors
- Verify Redis connection in worker
- Restart worker process
Problem: Port 8000 is already in use
Solutions:
- Find process using port:
lsof -i :8000 - Kill the process or use a different port
- Change port in uvicorn command:
--port 8001
Problem: Dependencies fail to install
Solutions:
- Update Poetry:
pip install --upgrade poetry - Clear cache:
poetry cache clear pypi --all - Delete
poetry.lockand reinstall - Check Python version:
python --version(should be 3.13)
Problem: Tests fail with Redis errors
Solutions:
- Ensure Redis is running and accessible
- Clear Redis data:
redis-cli FLUSHALL - Check test isolation (tests should clean up after themselves)
- Run tests individually to identify issues
The application uses minimal environment configuration:
REDIS_URL=redis://redis:6379 # Set in docker-composeFor local development without Docker, you may need to adjust Redis connection strings in:
app/services/todo_lists.pyapp/services/todo_items.pyapp/redis_config.pyapp/worker.py
Before deploying to production:
-
Security:
- Restrict CORS origins
- Add authentication/authorization
- Use environment variables for sensitive config
- Enable HTTPS
-
Redis:
- Use managed Redis service (AWS ElastiCache, Redis Cloud)
- Enable Redis persistence
- Configure password authentication
- Set up Redis clustering for high availability
-
Workers:
- Run multiple worker instances
- Use supervisor or systemd for process management
- Monitor worker health
- Implement retry logic for failed jobs
-
Monitoring:
- Add logging (structured logging)
- Set up error tracking (Sentry)
- Monitor Redis memory usage
- Track API performance metrics
When contributing to this project:
- Follow PEP 8 style guide (enforced by Ruff)
- Add type hints to all functions
- Write tests for new features
- Update documentation as needed
- Run linter and tests before committing:
poetry run ruff check .
poetry run mypy .
poetry run pytestRequest:
POST /api/todolists
Content-Type: application/json
{
"name": "Shopping List"
}Response (201 Created):
{
"id": 1,
"name": "Shopping List",
"items": []
}Request:
POST /api/todolists/1/items
Content-Type: application/json
{
"title": "Buy milk",
"description": "2 gallons of whole milk",
"completed": false
}Response (201 Created):
{
"id": 1,
"title": "Buy milk",
"description": "2 gallons of whole milk",
"completed": false
}Request:
POST /api/todolists/1/items/complete-allResponse (202 Accepted):
{
"message": "queued job",
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"todo_list_id": 1,
"check_status": "/api/jobs/550e8400-e29b-41d4-a716-446655440000"
}- Open in Dev Container (VS Code)
- Start FastAPI:
poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 - Start Worker:
poetry run python -m app.worker - Visit: http://localhost:8000/docs
- Run Tests:
poetry run pytest
Your backend is now ready to serve the React frontend! 🚀