A production-hardened FastAPI backend for tracking job applications, companies, recruiter contacts, status history, follow-up reminders, weekly job-search stats, and CSV export.
Job Tracker API lets a user manage a job search through REST endpoints. The API validates request and response data with Pydantic, persists normalized application data in PostgreSQL through SQLAlchemy models, and manages schema changes with Alembic migrations. It also includes production-style backend behaviour: request IDs, structured JSON request logs, standardized error responses, health/readiness checks, Docker Compose local development, seed data, and PostgreSQL-backed CI.
This project is intentionally kept as a $0 portfolio version. It is not deployed to a public free tier because free databases can expire, cold starts make demos unreliable, and the local Docker Compose workflow gives a more repeatable interview walkthrough.
- REST API design
- FastAPI routing
- Pydantic request/response validation
- SQLAlchemy ORM modelling
- PostgreSQL persistence
- Alembic migrations
- Docker Compose local environment
- pytest coverage
- PostgreSQL-backed CI
- Production API behaviour: request IDs, JSON logging, standardized errors
- Operational health/readiness endpoints
- FastAPI
- PostgreSQL
- SQLAlchemy
- Alembic
- pytest
- Docker
- GitHub Actions
- Create, read, update, and delete job applications
- Track company, role, location, application status, notes, and follow-up date
- Store companies in a first-class table linked from job applications
- Store recruiter/contact records linked to companies and applications
- Store first-class follow-up reminder records linked to applications
- Return weekly job-search statistics
- Export job applications as CSV
- Add request IDs to responses and structured request logs
- Filter applications by status, company, and follow-up date
- Store status changes over time in a status history table
- Add notes to each application
- Validate incoming API requests with Pydantic
- Run automated tests with pytest
- Run locally with Docker Compose
Client
-> FastAPI routers
-> Pydantic schemas
-> SQLAlchemy session/models
-> PostgreSQL
-> Alembic migrations
-> pytest + GitHub Actions verification
Alembic tracks database migrations, including the normalized companies and
contacts tables, follow-up reminder records, and application foreign keys.
GitHub Actions runs the pytest suite on pushes and pull requests.
| Table | Purpose |
|---|---|
companies |
Stores each company once so applications and contacts can link to a shared company record. |
contacts |
Stores recruiter or hiring contact details linked to a company. |
job_applications |
Stores the main job application record, including role, status, dates, notes, and optional company/contact links. |
application_status_history |
Stores status transitions so application progress can be audited over time. |
follow_up_reminders |
Stores durable follow-up reminders linked to applications. |
- Request ID middleware adds
X-Request-IDto every response - Structured JSON request logging records request ID, method, path, status code, and duration
- Standardized error responses wrap HTTP, validation, and unhandled errors
/healthchecks the API process;/readychecks database readiness- Pagination keeps list responses bounded
- CSV export provides a deterministic download path for applications
- Weekly stats summarize application activity and follow-up work
- PostgreSQL-backed CI verifies migrations and tests against a real database
- Repeatable seed data creates a local demo dataset without uncontrolled duplicates
Clone the repository:
git clone https://github.com/Dexter2099/Job-Tracker.git
cd Job-TrackerInstall dependencies:
pip install -r requirements.txtRun the API:
uvicorn app.main:app --reloadOpen the API docs:
http://127.0.0.1:8000/docs
The repo includes an intentionally thin server-rendered dashboard for portfolio videos. It exists only to make the FastAPI/PostgreSQL backend visible during a short demo; the API remains the main project surface.
Run it with:
uvicorn app.main:app --reloadOpen the dashboard:
http://127.0.0.1:8000/
API docs:
http://127.0.0.1:8000/docs
30-second recording script:
- Open
http://127.0.0.1:8000/and say this is a FastAPI/PostgreSQL backend with a thin dashboard only for visual demonstration. - Create one application from the form and point out that it persists through the same backend model path as the API.
- Change the status from
AppliedtoInterview, then filter byInterviewto show the status workflow. - Open
/docs,/health, and/readyto show the API contract and operational checks.
Run tests:
pytest -vRun database migrations:
alembic upgrade headSeed local demo data:
python scripts/seed_data.pyThe seed script creates a small fixed demo dataset for Swagger walkthroughs, weekly stats, reminders, and CSV export. It is safe to run more than once: known seed records are skipped instead of duplicated.
git clone https://github.com/Dexter2099/Job-Tracker.git
cd Job-Tracker
pip install -r requirements.txt
docker compose up --build
alembic upgrade head
python scripts/seed_data.pyThen open:
http://127.0.0.1:8000/docs
Run the test suite:
python -m pytest -vRun the API and PostgreSQL:
docker compose up --buildApply migrations after the database is running:
alembic upgrade headOpen the API docs:
http://127.0.0.1:8000/docs
Health check:
GET /health
Expected response:
{
"status": "ok"
}GET /health checks that the API process is alive.
GET /ready checks that the API can connect to the database with a simple
readiness query.
Ready response:
{
"status": "ready",
"database": "ok"
}Not ready response:
{
"status": "not_ready",
"database": "error"
}Every response includes an X-Request-ID header. If the client sends
X-Request-ID, the API reuses it; otherwise it generates a UUID4 value. Each
request also emits one JSON log line through the job_tracker.requests logger
with request_id, method, path, status_code, and duration_ms.
Handled errors use a consistent response shape:
{
"error": {
"code": "not_found",
"message": "Application not found",
"details": null,
"request_id": "..."
}
}Deployment is intentionally skipped to keep the project at $0 and avoid an unreliable portfolio link caused by free-tier cold starts, sleeping services, or expiring databases. The project can still be run locally with Docker Compose, and it demonstrates deployment readiness through Docker, Alembic migrations, health/readiness endpoints, GitHub Actions, and environment-based database configuration. No private job-tracker data is committed.
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Check API health |
GET |
/ready |
Check database readiness |
POST |
/applications |
Create a job application |
GET |
/applications |
List job applications |
GET |
/applications/export.csv |
Export job applications as CSV |
GET |
/applications/{id} |
Get one job application |
GET |
/applications/{id}/status-history |
List status history for one job application |
POST |
/applications/{id}/follow-up-reminders |
Create a follow-up reminder |
GET |
/applications/{id}/follow-up-reminders |
List follow-up reminders |
PATCH |
/applications/{id}/follow-up-reminders/{reminder_id} |
Mark a reminder completed |
PATCH |
/applications/{id} |
Partially update a job application |
DELETE |
/applications/{id} |
Delete a job application |
GET |
/stats/weekly |
Return weekly application and follow-up statistics |
GET /applications
Supported filters:
GET /applications?status=interview
GET /applications?company=Atlassian
GET /applications?follow_up_before=2026-06-15
GET /applications?needs_follow_up_by=2026-06-15
needs_follow_up_by returns applications with a follow-up date due on or before
the supplied date.
Pagination:
GET /applications?limit=20&offset=0
GET /applications?status=interview&limit=10&offset=20
limit defaults to 20 and accepts values from 1 to 100. offset
defaults to 0.
GET /applications/export.csv
GET /applications/export.csv?status=interview&company=Canva
The response is text/csv with a Content-Disposition attachment header and
these columns:
id,company,role_title,location,status,applied_date,follow_up_date,contact_name,contact_email,notes,created_at,updated_at
GET /applications/{id}
When an application's status changes, the API stores a row in
application_status_history with the previous status, new status, change
timestamp, and the update note when one is provided.
GET /applications/{id}/status-history
Status history is returned newest first.
Applications still support the simple follow_up_date field. For durable
follow-up tracking, reminders can also be stored as first-class records linked
to an application.
POST /applications/{id}/follow-up-reminders
Example request:
{
"reminder_date": "2026-06-18",
"note": "Send polite follow-up."
}GET /applications/{id}/follow-up-reminders
PATCH /applications/{id}/follow-up-reminders/{reminder_id}
Mark a reminder completed:
{
"completed": true
}GET /stats/weekly
GET /stats/weekly?start_date=2026-06-01&end_date=2026-06-07
Example response:
{
"week_start": "2026-06-01",
"week_end": "2026-06-07",
"applications_sent": 4,
"interviews": 1,
"rejections": 1,
"offers": 1,
"follow_ups_due": 2,
"overdue_follow_ups": 1
}POST /applications
The public API still accepts a company string for a job application. Internally,
the API creates or reuses a matching row in companies and links the application
with job_applications.company_id. Optional contact_name and contact_email
fields create or reuse a linked contacts row for that company.
Example request:
{
"company": "Atlassian",
"role_title": "Junior Backend Developer",
"location": "Sydney",
"job_url": "https://example.com/jobs/backend",
"status": "applied",
"source": "LinkedIn",
"contact_name": "Priya Shah",
"contact_email": "priya@example.com",
"salary_range": "$80,000-$95,000",
"notes": "Applied after tailoring resume.",
"follow_up_date": "2026-06-15",
"applied_date": "2026-05-31"
}Example response:
{
"id": 1,
"company": "Atlassian",
"role_title": "Junior Backend Developer",
"location": "Sydney",
"job_url": "https://example.com/jobs/backend",
"status": "applied",
"source": "LinkedIn",
"contact_name": "Priya Shah",
"contact_email": "priya@example.com",
"salary_range": "$80,000-$95,000",
"notes": "Applied after tailoring resume.",
"follow_up_date": "2026-06-15",
"applied_date": "2026-05-31",
"created_at": "2026-06-01T04:12:38.411147Z",
"updated_at": "2026-06-01T04:12:38.411147Z"
}PATCH /applications/{id}
Example request:
{
"status": "interview",
"contact_name": "Jordan Lee",
"contact_email": "jordan@example.com",
"notes": "Phone screen booked.",
"follow_up_date": "2026-06-20"
}Example response:
{
"id": 1,
"company": "Atlassian",
"role_title": "Junior Backend Developer",
"location": "Sydney",
"job_url": "https://example.com/jobs/backend",
"status": "interview",
"source": "LinkedIn",
"contact_name": "Jordan Lee",
"contact_email": "jordan@example.com",
"salary_range": "$80,000-$95,000",
"notes": "Phone screen booked.",
"follow_up_date": "2026-06-20",
"applied_date": "2026-05-31",
"created_at": "2026-06-01T04:12:38.411147Z",
"updated_at": "2026-06-01T04:12:38.446502Z"
}DELETE /applications/{id}
Successful deletes return:
204 No Content
Job Tracker API is a FastAPI/PostgreSQL backend for managing job applications, companies, recruiter contacts, status history, follow-up reminders, weekly stats, and CSV export.
When a client sends a request, FastAPI routes it to the correct handler, Pydantic validates the request and response shape, SQLAlchemy uses a database session to read or write models, PostgreSQL stores the data, and Alembic keeps schema changes versioned. The test suite verifies the API locally and in PostgreSQL-backed CI.
I normalized companies and contacts because the same company or recruiter can appear across multiple applications, and duplicating those fields inside every application would make the data harder to maintain. Status history and follow-up reminders make the domain model stronger: they preserve how an application changed over time and turn follow-up work into durable records instead of a single date field.
I hardened the API with request IDs, structured JSON logs, standardized error
responses, pagination, CSV export, seed data, and separate /health and
/ready endpoints. I intentionally left out auth and public deployment for this
portfolio version so the project stays focused, free to run, and easy to demo
locally. A deliberate production-realism path would add auth and user-owned
data next.