A simple FastAPI backend used to track job applications, companies and follow-ups. This project is designed as a technical portfolio piece with:
- authentication (register/login) using JWT
- company & application CRUD operations
- follow-up notes tied to applications
- SQLite fallback for local development and testing
- SQLAlchemy + Alembic for data models and migrations
flowchart LR
subgraph API
A[FastAPI App]
R[Auth / Companies / Applications / Followups]
M[RequestIdMiddleware<br/>adds request_id]
A -->|includes routers| R
A --> M
end
A --> DB[(Database)]
- Argon2 for password hashing – new passwords are hashed with Argon2 (memory-hard, GPU-resistant). bcrypt is still accepted for backward
compatibility. The helper
needs_rehash()can be used to transparently upgrade legacy hashes on successful login - JWT with HS256 – symmetric signing keeps the implementation
simple; tokens contain only the user email (
sub) and expiration. HS256 is widely supported and appropriate for a single‑service API.
Request logging is structured and enriched with a request_id from
app/middleware/request_id.py. Every handler can include this ID in
logs to trace individual requests across async calls.
{
"sub": "user@example.com",
"exp": 1740000000
}(The access_token is a base64‑encoded header.payload.signature string.)
# clone & prepare
git clone <repo> job-tracker-api && cd job-tracker-api
python -m venv .venv
.venv\Scripts\activate # windows
pip install -e .[dev]
# run the app on localhost:8000 (default SQLite)
uvicorn app.main:app --reload &
# register/login, create company & application
curl -s -X POST http://localhost:8000/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"me@example.com","password":"pass1234"}'
TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=me@example.com&password=pass1234' | jq -r .access_token)
# create company and an application
curl -s -X POST http://localhost:8000/companies/ \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"Acme","website":"https://acme"}' | jq
curl -s -X POST http://localhost:8000/applications/ \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"position":"Dev","company_id":1}' | jq
# filter, patch, dashboard
curl -s http://localhost:8000/applications/?status=applied \
-H "Authorization: Bearer $TOKEN" | jq
curl -s -X PATCH http://localhost:8000/applications/1 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"status":"interview"}' | jq
curl -s http://localhost:8000/applications/dashboard/summary \
-H "Authorization: Bearer $TOKEN" | jqThe rest of this README documents the architecture, configuration, and additional endpoints. The above commands are sufficient to verify the API is running.
python -m venv .venv
.venv\Scripts\activate # Windows
# or source .venv/bin/activate on macOS/Linux
pip install -e .[dev]The pyproject.toml defines dependencies and ruff for linting.
A .pre-commit-config.yaml is provided to run ruff and cleanup whitespace automatically.
Settings come from environment variables or a .env file.
The following variables are recognized (defaults shown):
| Name | Default | Description |
|---|---|---|
ENV |
local |
application environment (test triggers in-memory SQLite) |
SECRET_KEY |
change-me |
JWT signing key |
DATABASE_URL |
computed | full SQLAlchemy URL, fallback to SQLite if not set |
For a simple local run you can leave DATABASE_URL unset and a file
./db.sqlite3 will be used automatically. Tests set ENV=test and
use an in-memory database.
uvicorn app.main:app --reloadA simple Makefile offers shortcuts:
make test # run pytest
make lint # run ruff and fix issues
make up # start services via docker-compose (prod-like)There are two compose configs:
docker-compose.yml– production‑like, builds the image, runsapianddbwithout mounting source. Use for CI or when you want a clean container.docker-compose.dev.yml– development mode; source is bind-mounted and the entrypoint script runs migrations then starts uvicorn. It also brings up a local Postgres instance. If thealembic upgrade headstep fails the container exits (due toset -e), which is the desired behaviour.
Start dev: docker compose -f docker-compose.dev.yml up --build.
The entrypoint (scripts/entrypoint.sh) runs migrations and then launches
uvicorn; in dev it simply logs the failure and aborts if the migration step
fails, ensuring you don’t forget to apply schema changes.
API docs available at http://localhost:8000/docs.
pip install -e .[dev]
pytest -qThe test suite includes both unit and integration tests. A shared
tests/conftest.py sets up a temporary SQLite database and adjusts
FastAPI dependencies so the code under test uses the right engine.
ruff check --fix .Alembic is configured in alembic/. To create a migration:
alembic revision --autogenerate -m "describe change"
alembic upgrade head| Method | Path | Params | Description |
|---|---|---|---|
| POST | /auth/register |
– | create user |
| POST | /auth/login |
– | obtain bearer token |
| GET | /health |
– | healthcheck (executes SELECT 1) |
| GET | /metrics |
– | Prometheus metrics |
| GET | /companies/ |
– | list companies |
| POST | /companies/ |
– | create company |
| DELETE | /companies/{id} |
– | delete company |
| GET | /applications/ |
status, company_id, limit, offset, order_by, desc |
list with paging/filter/sort |
| POST | /applications/ |
– | create application |
| PATCH | /applications/{id} |
– | partial update |
| DELETE | /applications/{id} |
– | delete application |
| GET | /applications/dashboard/summary |
– | counts by status + recent followups |
| GET | /followups/ |
application_id |
list notes for app |
| POST | /followups/ |
– | create followup note |
| DELETE | /followups/{id} |
– | delete note |
Authentication is required for most endpoints. Use the returned JWT in
Authorization: Bearer <token> header.
The login endpoint applies a simple in-memory rate limit (5 attempts per minute per email address, sliding window). This is a demo‑only guard; if the email changes the counter resets. A real system would combine IP-based throttling or use a shared cache.
Designed for portfolio/demo use: defaults favor fast local execution (SQLite) and deterministic tests. For production: configure Postgres, rotate secrets, enforce HTTPS, and consider asymmetric JWT signing.