Note that alongside my submission, I will be attaching a detailed explanation on my thought process and everything I have done. This is mostly focused on how to run, and the basics.
Small Python service that fetches HubSpot contacts and exposes them through a clean internal API at GET /contacts. OAuth2 token handling is isolated behind a Nango adapter, so the rest of the codebase never handles raw tokens directly.
Prerequisites:
- Python 3.10+
- pip
Clone the repository:
git clone https://github.com/egehankocak99/test.verea.git
cd test.vereaCreate a virtual environment:
python -m venv .venvActivate it.
Windows PowerShell:
.\.venv\Scripts\Activate.ps1Mac/Linux:
source .venv/bin/activateInstall dependencies:
pip install -r requirements.txtCreate the local environment file.
Windows PowerShell:
Copy-Item .env.example .envMac/Linux:
cp .env.example .envRun the service:
uvicorn app.main:app --reloadAlternative: run with Docker:
docker-compose up --buildOpen these URLs:
http://localhost:8000/healthhttp://localhost:8000/docs
Local behavior with the default mock setup:
GET /healthreturns200with{"status": "ok"}GET /contactsreturns401with{"detail": "HubSpot token is expired or invalid."}becauseUSE_MOCK_TOKEN=truereturns a fake token intentionally
Run the tests:
pytest tests/ -vVerified result:
21 passed in 0.25s
The service is split into clear layers: router.py handles HTTP, service.py orchestrates token retrieval and contact fetching, repository.py talks to HubSpot and normalises raw payloads, and nango/client.py handles token retrieval only. The main design pattern used is the Adapter pattern in the token layer through TokenProvider, MockTokenProvider, and NangoTokenProvider. Light DDD thinking is applied by separating raw HubSpot responses from the internal Contact entity.
This version uses a mock token provider by default, so the integration boundary is demonstrated without requiring a live Nango account.
-
Pagination — highest priority. Add a cursor loop in
repository.pythat follows HubSpot'spaging.next.afterfield until there are no more pages. The interface to the rest of the app stays identical —fetch_contacts()still returns a flat list — the loop is entirely internal to the repository. -
Retry with exponential backoff on 429. Wrap the HubSpot call in a retry decorator that reads the
Retry-Afterheader from the 429 response and waits that many seconds before retrying, up to a configurable maximum number of attempts. After that it raises as it does today. -
Response caching with a TTL. Add a simple in-memory cache on
fetch_contacts()with a 60 second TTL. For a more production-grade setup, replace the in-memory cache with Redis so the cache survives service restarts and works across multiple instances. -
API key authentication middleware. Add a FastAPI middleware that checks for a valid API key in the
X-API-Keyheader on every request. Keys stored in environment variables, checked before the request reaches any endpoint. -
Real Nango connection. Register a Nango account, create a HubSpot OAuth2 integration, store the connection ID in
.env, and flipUSE_MOCK_TOKEN=false. TheNangoTokenProviderclass is already implemented — this is purely a credentials and configuration step, no code changes needed. -
Structured logging. Add Python's standard
loggingmodule configured to output JSON-formatted log lines. Log every incoming request, every outbound HubSpot call with its response time, and every error with full context. This makes the service debuggable in production without needing to reproduce issues locally. -
docker-compose.yml. A single
docker-compose.ymlthat builds the service image and runs it with the correct environment variables. One command to go from a fresh clone to a running service — removes all manual setup steps from the README. -
Contact field validation.* Add an email format validator to the Contact model using Pydantic's
EmailStrtype. Add a separate validation step in the repository that flags contacts with both empty name and empty email as suspect rather than passing them through silently.
I completed the implementation in step-sized parts, but I did not create commits at the exact moment each step was finished. Before submission, I reconstructed the history into the same logical steps with descriptive commit messages so the development flow remains easy to review. Readme file was also committed at last.
Docker Update:
I also added a Dockerfile and docker-compose.yml for easier local setup and testing. This Docker setup was written manually.