Production-grade REST API for space and room booking management — coworking, meeting rooms, consultation slots.
📖 booking.lsbstack.com/docs — Swagger UI (interactive, try it live)
Option 1 — Admin user (full access)
- Open
POST /auth/login→ click Try it out → use these credentials:
{
"email": "adminlsb@bookingapi.com",
"password": "Admin1234!"
}- Copy the
access_tokenfrom the response - Click Authorize (top right) → paste the token → Authorize
- All endpoints including admin (create spaces, list all bookings, manage users) are now unlocked
Option 2 — Regular user
- Open
POST /auth/register→ click Try it out → register with any email and password - Copy the
access_tokenfrom the response - Click Authorize → paste the token → Authorize
- Bookings endpoints are now available
BookingAPI is a portfolio project that demonstrates building a production-grade Python backend from scratch. It implements:
- Hexagonal Architecture (Ports & Adapters) — strict dependency inversion
- Async FastAPI with PostgreSQL, Redis, and Celery
- JWT authentication with refresh token rotation in HttpOnly cookies
- Availability caching with Redis, invalidated on mutations
- Background workers (booking confirmations, reminders, expired booking cleanup)
- Kubernetes deployment with HPA, zero-downtime rolling updates, and NetworkPolicy
- Full CI/CD pipeline with GitHub Actions, ECR, and release-please
BookingAPI follows Hexagonal Architecture. The dependency rule is absolute: inner layers never import from outer layers.
graph TD
subgraph API["API Layer (FastAPI)"]
R[Routers]
S[Schemas]
D[Dependency Injection]
end
subgraph APP["Application Layer"]
UC[Use Cases]
DTO[DTOs]
end
subgraph DOMAIN["Domain Layer (pure Python)"]
E[Entities: Booking · Space · User]
VO[Value Objects]
EV[Domain Events]
P[Ports — Protocol interfaces]
end
subgraph INFRA["Infrastructure Layer"]
DB[SQLAlchemy Repos]
RD[Redis Cache]
CL[Celery Workers]
SEC[JWT · bcrypt]
end
API --> APP
APP --> DOMAIN
INFRA --> DOMAIN
API --> INFRA
style DOMAIN fill:#1a1a2e,color:#e0e0e0,stroke:#4a90d9
style APP fill:#16213e,color:#e0e0e0,stroke:#4a90d9
style API fill:#0f3460,color:#e0e0e0,stroke:#4a90d9
style INFRA fill:#533483,color:#e0e0e0,stroke:#4a90d9
Key design decisions:
- Domain entities are pure Python — no SQLAlchemy, no Pydantic, no FastAPI imports
- SQLAlchemy models and domain entities are always separate classes connected by mappers
- Ports are defined as
Protocolinterfaces indomain/ports/— infrastructure implements them, never the reverse - Application DTOs use plain dataclasses, not Pydantic (Pydantic is HTTP-only)
| Layer | Technology |
|---|---|
| Language | Python 3.12 |
| Package manager | uv |
| API | FastAPI + Pydantic v2 |
| Database | PostgreSQL 16 + SQLAlchemy 2.0 async |
| Migrations | Alembic |
| Cache / broker | Redis 7 |
| Background jobs | Celery + celery-beat |
| Auth | PyJWT + bcrypt (async via thread executor) |
| Rate limiting | slowapi |
| Logging | structlog (JSON, with request ID) |
| Linting | Ruff |
| Type checking | mypy strict |
| Testing | pytest + httpx + factory-boy + pytest-asyncio |
| Containers | Docker + docker-compose |
| Orchestration | Kubernetes / k3s |
| CI/CD | GitHub Actions → Amazon ECR → kubectl rolling update |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/auth/register |
Register a new user | — |
POST |
/auth/login |
Login — returns access token + sets refresh cookie | — |
POST |
/auth/refresh |
Rotate access token using HttpOnly refresh cookie | Cookie |
POST |
/auth/logout |
Clear refresh cookie | Cookie |
POST |
/auth/password-reset/request |
Request password reset email | — |
POST |
/auth/password-reset/confirm |
Confirm reset with token | — |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/spaces |
List active spaces | — |
GET |
/spaces/{id} |
Get space details | — |
GET |
/spaces/{id}/availability |
Check availability for a time slot (cached) | — |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/bookings |
Create a booking | Bearer |
GET |
/bookings |
List my bookings (filterable by status) | Bearer |
GET |
/bookings/{id} |
Get booking details | Bearer |
PATCH |
/bookings/{id} |
Update booking time slot | Bearer |
DELETE |
/bookings/{id} |
Cancel booking | Bearer |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/admin/spaces |
Create a space | Admin |
PATCH |
/admin/spaces/{id} |
Update space attributes | Admin |
DELETE |
/admin/spaces/{id} |
Deactivate a space | Admin |
GET |
/admin/bookings |
List all bookings (filterable) | Admin |
PATCH |
/admin/users/{id} |
Update user (max bookings, admin flag) | Admin |
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Liveness probe |
GET |
/health/ready |
Readiness probe (checks database connectivity) |
- Docker + Docker Compose
- Python 3.12+
- uv
git clone https://github.com/LucasBenitez7/booking-api.git
cd booking-api
# Copy and configure environment
cp .env.example .env
# Start services (PostgreSQL + Redis) and the API
docker compose upThe API is available at http://localhost:8000
Swagger docs at http://localhost:8000/docs
# Install dependencies (includes dev extras)
uv sync --extra dev
# Start PostgreSQL and Redis separately, then:
uv run uvicorn booking.api.main:app --reloaduv run alembic upgrade head- Import
postman/environment.json, thenpostman/collection.json, into Postman. - Select the environment BookingAPI — Local (
base_urldefaults tohttp://localhost:8000). - Run 0 — Setup / Reset — wipes the database for a clean start.
- Run 0 — Setup / Seed — creates an admin user, a regular user, and 3 spaces. Saves
admin_tokenautomatically. - Run the folders in order: Health → Auth → Spaces → Bookings → Admin.
All tokens and IDs are saved automatically by test scripts — no manual steps required.
Run the entire collection from the terminal (Newman):
npx newman run postman/collection.json -e postman/environment.jsonEmail notifications: this project uses LoggingNotificationService — confirmation and reminder emails are printed to stdout (application logs) instead of being sent. Swapping in a real SMTP or SendGrid adapter requires changing only one line in app_factory.py; the domain and application layers are unaffected by design.
Note: the /dev/seed and /dev/reset endpoints are only available when APP_ENV=development. They do not exist in production.
To regenerate the collection after API changes:
uv run python scripts/generate_postman_collection.py# Run all unit tests
uv run pytest tests/unit/
# Run with coverage report
uv run pytest tests/unit/ --cov=src/booking --cov-report=html
# Run integration tests (requires PostgreSQL + Redis running)
uv run pytest tests/integration/Coverage note: the
--cov-fail-under=80gate in CI measures coverage over the domain and application layers only. HTTP routers, SQLAlchemy repositories, Celery workers, and the Redis adapter are excluded from the metric because they require a running infrastructure environment — those layers are exercised by integration tests and staging. This is a deliberate choice, not a gap.
postman/ # Postman collection + environment (import into Postman)
scripts/ # generate_postman_collection.py · run_newman.py
src/booking/
├── domain/ # Pure Python — zero external dependencies
│ ├── entities/ # Booking, Space, User
│ ├── value_objects/ # BookingId, BookingStatus, TimeSlot, Email
│ ├── events/ # BookingCreated, BookingCancelled, BookingExpired…
│ ├── exceptions/ # DomainException hierarchy
│ └── ports/ # Protocol interfaces (contracts for infrastructure)
├── application/
│ ├── use_cases/ # CreateBooking, CancelBooking, GetAvailability…
│ └── dtos/ # Input/output dataclasses (no Pydantic)
├── infrastructure/
│ ├── persistence/ # SQLAlchemy models, mappers, repositories
│ ├── cache/ # RedisAvailabilityCache, MemoryAvailabilityCache
│ ├── workers/ # Celery app, tasks, beat schedule
│ ├── notifications/ # LoggingNotificationService
│ └── security/ # JWT, bcrypt, password reset store
└── api/
├── routers/ # auth, bookings, spaces, admin, health
├── schemas/ # Pydantic request/response models
├── middleware/ # CORS, security headers, request ID logging
├── deps.py # FastAPI dependency injection
└── app_factory.py # Application factory with lifespan
Bookings are confirmed immediately — no PENDING state
When a booking is created it goes directly to CONFIRMED. There is no intermediate state to manage or clean up.
Business rules live in the Space entity
Space enforces: min_duration_minutes, max_duration_minutes, min_advance_minutes, cancellation_deadline_hours, opening_time, closing_time. A booking cannot be created without passing space.validate_booking_slot().
EXPIRED is a distinct terminal state from CANCELLED
Bookings whose end time passes without cancellation are marked EXPIRED by a Celery task — not cancelled. This preserves audit intent: a system expiration is semantically different from a user or admin cancellation.
Availability is cached, not computed on every request
GET /spaces/{id}/availability stores its result in Redis with a 5-minute TTL. Any mutation (create, update, cancel booking) invalidates the cache for that space via SCAN pattern matching. An in-memory fallback is used when Redis is not configured.
Refresh token in HttpOnly cookie
Access tokens (15 min) are returned in the response body. Refresh tokens (7 days) are stored in an HttpOnly cookie — never accessible from JavaScript, mitigating XSS token theft.