By Priyadharshiny J — GitHub: priyadharshiny13
Overview
Smart Health API is a backend platform I built to explore how a real healthcare product might structure its API layer: role-based access for different user types, secure authentication, database migrations that don't break production, and a machine learning model wired into an async pipeline instead of blocking the request thread.
The result is a FastAPI service that lets Patients, Doctors, and Admins interact with the same system through different permission levels, and that returns CNN-based oral cancer risk scores without making the caller wait on model inference.
Stack
- API layer: FastAPI
- Database: MySQL, with SQLAlchemy ORM
- Migrations: Alembic
- Auth: JWT-based authentication with Role-Based Access Control (RBAC)
- ML: TensorFlow CNN model for oral cancer risk prediction
- Async processing: Celery + Redis
- Infra: Docker, deployed toward AWS
- CI/CD: GitHub Actions
Architecture Decisions
Why RBAC with three roles instead of a single user type?
Healthcare data access isn't uniform — a Patient should only see their own records, a Doctor needs read/write access to their assigned patients, and an Admin needs oversight without clinical permissions. I modeled this directly into the JWT claims and enforced it at the route-dependency level in FastAPI, rather than checking roles inside each endpoint's business logic. This keeps permission logic in one place and out of the individual handlers.
Why Alembic migrations instead of create_all()?
SQLAlchemy.create_all() is fine for a prototype, but it can't handle schema evolution — adding a column, renaming a table, backfilling data. Alembic gave the project versioned, reversible migrations, which is the same discipline a real production service needs the moment more than one person touches the schema.
Why Celery + Redis for the CNN inference?
Running a CNN prediction synchronously inside an HTTP request handler blocks the worker for however long inference takes, which doesn't scale under concurrent load. I moved prediction into a Celery task queue backed by Redis: the API accepts the image, enqueues the job, and returns a task ID immediately. The client polls (or would be pushed a webhook/notification in a fuller version) for the result. This separates the request/response cycle from the compute-heavy ML work.
A Real Debugging Story
Wiring up SQLAlchemy models across multiple modules (User, Patient, Doctor, Prediction) introduced circular import errors once relationships started referencing each other — Patient needed User, User needed Prediction, and Python's import system didn't like the cycle. The fix was restructuring model definitions to use string-based relationship references (relationship("Patient", back_populates=...)) instead of direct class imports, which lets SQLAlchemy resolve the relationship lazily at mapper-configuration time rather than at import time.
Small detail, but it's the kind of thing that only shows up once a schema has enough interrelated tables — and it's a good reminder that ORM relationship design needs the same intentionality as the schema itself.
What's Next
- Add automated tests for the RBAC permission boundaries (currently manually verified)
- Expose Celery task status through a WebSocket instead of polling
- Add rate limiting on the prediction endpoint given its compute cost
Repo
Code and setup instructions: github.com/priyadharshiny13 — see the smart-health-api repository.
Top comments (0)