A minimal yet production-grade Python service for managing a bookstore catalogue. Built with FastAPI, PostgreSQL, and Redis caching.
- RESTful API for book management (create, get, search)
- PostgreSQL database with proper indexing
- Multi-level caching (Redis + in-process LRU fallback)
- Cursor-based pagination for scalability
- Type-safe code with Pydantic and mypy
- Comprehensive test suite
- Docker and docker-compose support
- Database migrations with Alembic
- Structured logging
- Prometheus metrics
- Installation
- Running Locally
- Running with Docker
- Environment Variables
- Database Migrations
- API Documentation
- API Examples (cURL)
- Troubleshooting
- Python: 3.11 or higher
- PostgreSQL: 12 or higher
- Redis: 6.0 or higher (required)
- Docker & Docker Compose: (optional, for containerized deployment)
git clone https://github.com/isivaselvan/mini-bookstore-svc.git
cd mini-bookstore-svc# Using make
make venv
# Or manually
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate# Install with development dependencies
make install-dev
# Or manually
pip install -e ".[dev]"Create a .env file in the project root:
# Create .env file manually or copy from example
cat > .env << EOF
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bookstore
REDIS_URL=redis://localhost:6379
LOG_LEVEL=INFO
CACHE_TTL_BOOK=300
CACHE_TTL_SEARCH=60
EOFEdit .env with your configuration (see Environment Variables section).
-
Create PostgreSQL database:
createdb bookstore # Or using psql: psql -U postgres -c "CREATE DATABASE bookstore;"
-
Run migrations:
make upgrade # Or manually: alembic upgrade head
-
Start PostgreSQL (if not running):
# macOS (using Homebrew) brew services start postgresql # Linux (using systemd) sudo systemctl start postgresql # Or use Docker docker run -d --name postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=bookstore \ -p 5432:5432 \ postgres:15-alpine
-
Start Redis (if not running):
# macOS (using Homebrew) brew services start redis # Linux (using systemd) sudo systemctl start redis # Or use Docker docker run -d --name redis \ -p 6379:6379 \ redis:7-alpine
-
Start the API:
make run # Or manually: bookstore-api # Or using uvicorn directly: uvicorn mini_bookstore_svc.main:app --reload --host 0.0.0.0 --port 8000
The API will be available at:
- API:
http://localhost:8000 - API Docs (Swagger):
http://localhost:8000/docs - API Docs (ReDoc):
http://localhost:8000/redoc - Health Check:
http://localhost:8000/healthz - Metrics:
http://localhost:8000/metrics
-
Start all services (API, PostgreSQL, Redis):
make docker-up # Or manually: docker-compose -f docker/docker-compose.yml up -d -
Run database migrations:
docker-compose -f docker/docker-compose.yml exec api alembic upgrade head -
Check service status:
docker-compose -f docker/docker-compose.yml ps
-
View logs:
# All services docker-compose -f docker/docker-compose.yml logs -f # Specific service docker-compose -f docker/docker-compose.yml logs -f api
-
Stop services:
make docker-down # Or manually: docker-compose -f docker/docker-compose.yml down
All configuration is done via environment variables. You can set them in a .env file or export them in your shell.
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://postgres:postgres@localhost:5432/bookstore |
REDIS_URL |
Redis connection URL | redis://localhost:6379 |
| Variable | Description | Default |
|---|---|---|
LOG_LEVEL |
Logging level (DEBUG, INFO, WARNING, ERROR) | INFO |
CACHE_TTL_BOOK |
TTL for book cache in seconds | 300 (5 minutes) |
CACHE_TTL_SEARCH |
TTL for search cache in seconds | 60 (1 minute) |
APP_NAME |
Application name | Mini Bookstore Service |
DEBUG |
Enable debug mode (boolean) | False |
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bookstore
# Redis
REDIS_URL=redis://localhost:6379
# Logging
LOG_LEVEL=INFO
# Cache TTLs
CACHE_TTL_BOOK=300
CACHE_TTL_SEARCH=60
# Application
APP_NAME=Mini Bookstore Service
DEBUG=falsePostgreSQL:
postgresql://[user]:[password]@[host]:[port]/[database]
Redis:
redis://[host]:[port]/[db_number]
# Or with password:
redis://:[password]@[host]:[port]/[db_number]
This project uses Alembic for database migrations.
make migrate MESSAGE="add_new_column"
# Or manually:
alembic revision --autogenerate -m "add_new_column"make upgrade
# Or manually:
alembic upgrade headmake downgrade
# Or manually:
alembic downgrade -1alembic historyalembic current# Apply migrations
docker-compose -f docker/docker-compose.yml exec api alembic upgrade head
# Create migration
docker-compose -f docker/docker-compose.yml exec api alembic revision --autogenerate -m "description"
# Rollback
docker-compose -f docker/docker-compose.yml exec api alembic downgrade -1- Local:
http://localhost:8000 - Docker:
http://localhost:8000
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
| Method | Endpoint | Description |
|---|---|---|
POST |
/v1/books |
Create a new book |
GET |
/v1/books/{id} |
Get book by ID |
GET |
/v1/books |
Search books with filters and pagination |
GET |
/healthz |
Health check |
GET |
/metrics |
Prometheus metrics |
GET |
/ |
API information |
curl -X POST "http://localhost:8000/v1/books" \
-H "Content-Type: application/json" \
-d '{
"title": "Python Programming",
"author": "John Doe",
"price": {
"currency": "USD",
"amount": 2999
},
"tags": ["python", "programming", "tech"],
"published_at": "2024-01-15T10:00:00Z"
}'Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Python Programming",
"author": "John Doe",
"price": {
"currency": "USD",
"amount": 2999
},
"tags": ["python", "programming", "tech"],
"published_at": "2024-01-15T10:00:00Z",
"created_at": "2024-01-20T12:00:00Z",
"updated_at": "2024-01-20T12:00:00Z"
}# Replace {id} with actual book ID from previous response
curl "http://localhost:8000/v1/books/550e8400-e29b-41d4-a716-446655440000"Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Python Programming",
"author": "John Doe",
"price": {
"currency": "USD",
"amount": 2999
},
"tags": ["python", "programming", "tech"],
"published_at": "2024-01-15T10:00:00Z",
"created_at": "2024-01-20T12:00:00Z",
"updated_at": "2024-01-20T12:00:00Z"
}curl "http://localhost:8000/v1/books?query=python&limit=10&sort=title:asc"Response:
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Python Programming",
"author": "John Doe",
...
}
],
"next": "eyJpZCI6IjU1MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMCIsImxpbWl0IjoxMCwic29ydCI6InRpdGxlOmFzYyJ9"
}curl "http://localhost:8000/v1/books?tag=tech&tag=programming&limit=10&sort=title:asc"# Use the 'next' token from previous response
curl "http://localhost:8000/v1/books?page=eyJpZCI6IjU1MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMCIsImxpbWl0IjoxMCwic29ydCI6InRpdGxlOmFzYyJ9&limit=10&sort=title:asc"curl "http://localhost:8000/v1/books?query=python&tag=tech&limit=5&sort=published_at:desc"curl "http://localhost:8000/healthz"Response:
{
"ok": true
}curl "http://localhost:8000/metrics"Response:
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{endpoint="/v1/books",method="GET",status="200"} 10.0
...
curl "http://localhost:8000/"Response:
{
"name": "Mini Bookstore Service",
"version": "0.1.0",
"docs": "/docs",
"health": "/healthz"
}Error:
sqlalchemy.exc.OperationalError: could not connect to server
Solutions:
- Verify PostgreSQL is running:
pg_isreadyorpsql -U postgres - Check
DATABASE_URLin.envfile - Verify database exists:
psql -U postgres -l | grep bookstore - Check firewall/network settings
- For Docker: Ensure containers are on the same network
Error:
redis.exceptions.ConnectionError: Error connecting to Redis
Solutions:
- Verify Redis is running:
redis-cli ping(should returnPONG) - Check
REDIS_URLin.envfile - Verify Redis port (default: 6379) is not blocked
- For Docker: Ensure containers are on the same network
Error:
alembic.util.exc.CommandError: Target database is not up to date
Solutions:
- Check current migration:
alembic current - View migration history:
alembic history - Apply pending migrations:
alembic upgrade head - If needed, rollback:
alembic downgrade -1
Error:
OSError: [Errno 48] Address already in use
Solutions:
- Find process using port 8000:
lsof -i :8000(macOS/Linux) - Kill the process:
kill -9 <PID> - Or use a different port:
uvicorn mini_bookstore_svc.main:app --port 8001
-
Enable Debug Logging:
export LOG_LEVEL=DEBUG # Or in .env file: LOG_LEVEL=DEBUG
-
Check Application Logs:
# Local - logs are output to stdout/stderr # Docker docker-compose -f docker/docker-compose.yml logs -f api
-
Test Database Connection:
psql $DATABASE_URL -c "SELECT 1;"
-
Test Redis Connection:
redis-cli -u $REDIS_URL ping
mini-bookstore-svc/
├── src/
│ └── mini_bookstore_svc/
│ ├── __init__.py
│ ├── version.py
│ ├── config.py # Configuration management
│ ├── logging.py # Structured logging
│ ├── errors.py # Custom exceptions
│ ├── models.py # Pydantic models
│ ├── domain.py # Business logic
│ ├── repo_sql.py # Repository layer
│ ├── cache.py # Caching layer
│ ├── api.py # FastAPI routers
│ └── main.py # Application factory
├── tests/
├── migrations/ # Alembic migrations
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── pyproject.toml
├── Makefile
└── README.md
make help # Show all available commands
make venv # Create virtual environment
make install # Install production dependencies
make install-dev # Install development dependencies
make run # Run the application
make lint # Run linter
make fmt # Format code
make type-check # Run type checker
make test # Run tests
make test-cov # Run tests with coverage
make migrate # Create migration (use MESSAGE="desc")
make upgrade # Apply migrations
make downgrade # Rollback migration
make docker-build # Build Docker image
make docker-up # Start Docker services
make docker-down # Stop Docker services