A REST API built with FastAPI, SQLAlchemy, and PostgreSQL, covering posts, user accounts, JWT authentication, and post voting.
A companion Streamlit frontend for this API lives in a separate repo: frontend-development-course. It's fully decoupled — it only talks to this API over HTTP — so the two are developed, versioned, and deployed independently. See that repo's README for setup.
- CRUD operations on posts (title, content, published flag)
- User registration and JWT-based login
- Post ownership enforcement (only the creator can update/delete a post)
- Upvoting/downvoting posts, with vote counts returned alongside each post
- Search, pagination (
limit/skip), and title filtering on the posts list - Database schema managed with Alembic migrations
- Config and secrets loaded from environment variables (never hardcoded)
- FastAPI – web framework
- SQLAlchemy – ORM
- Alembic – database migrations
- PostgreSQL – database
- Pydantic / pydantic-settings – request/response schemas and config
- PyJWT + pwdlib – authentication and password hashing
- Docker – containerization
- GitHub Actions – CI (lint + test) and image publishing
- Ruff – linting and formatting
app/
├── main.py # FastAPI app instance, router registration
├── config.py # Settings loaded from .env via pydantic-settings
├── database.py # SQLAlchemy engine/session setup
├── models.py # SQLAlchemy ORM models (Post, User, Vote)
├── schemas.py # Pydantic request/response schemas
├── oauth2.py # Password hashing, JWT creation/validation
└── routers/
├── post.py # /posts endpoints
├── user.py # /users endpoints
├── auth.py # /login endpoint
└── vote.py # /vote endpoints
alembic/ # Database migration scripts
.github/workflows/ # CI/CD pipeline (lint, test, build, publish)
Dockerfile # API container image
docker-compose-dev.yml # Local dev stack (API + Postgres, live reload)
docker-compose-prod.yml # Production stack (pre-built image, no bind mounts)
requirements.txt # Runtime dependencies (what Dockerfile installs)
requirements-dev.txt # + test/lint tooling, for local dev and CI
ruff.toml # Lint/format configuration
There are two ways to run this project: with Docker (recommended, no local Postgres install needed) or fully locally.
1. Configure environment variables
Create a .env file in the project root:
DATABASE_HOSTNAME=localhost
DATABASE_PORT=5433
DATABASE_NAME=api_development_course
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=<your-db-password>
SECRET_KEY=<a-random-secret-key>
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<same-as-DATABASE_PASSWORD>
POSTGRES_DB=api_development_course.env is gitignored — never commit real credentials. The Postgres container's host port is mapped to 5433 (not the default 5432) to avoid clashing with a locally installed PostgreSQL service, if you have one.
2. Start the containers
docker compose -f docker-compose-dev.yml up --buildThis builds the API image, starts Postgres, and runs uvicorn with --reload (code changes on your machine are picked up live via a bind mount).
3. Run database migrations
alembic upgrade headMigrations run from your host machine against the containerized Postgres via the 5433 port mapping above.
The API is now available at http://127.0.0.1:8000, with interactive docs at http://127.0.0.1:8000/docs.
1. Prerequisites
- Python 3.13+
- A running PostgreSQL instance
2. Install dependencies
python -m venv .venv
source .venv/Scripts/activate # Windows Git Bash
pip install -r requirements-dev.txtrequirements-dev.txt pulls in requirements.txt plus test/lint tooling (pytest, ruff). If you only need to run the API itself (e.g. building the Docker image), requirements.txt alone is enough — that's what Dockerfile installs.
3. Configure environment variables
Same as above, but set DATABASE_PORT=5432 (or whatever port your local Postgres actually listens on) and DATABASE_HOSTNAME=localhost.
4. Run database migrations
alembic upgrade head5. Start the server
uvicorn app.main:app --reload| Method | Endpoint | Description | Auth required |
|---|---|---|---|
| POST | /users/ |
Register a new user | No |
| GET | /users/{id} |
Get a user by id | No |
| POST | /login/ |
Log in, receive a JWT | No |
| GET | /posts/ |
List posts (supports search, limit, skip) |
Yes |
| GET | /posts/{id} |
Get a single post | Yes |
| POST | /posts/ |
Create a post | Yes |
| PUT | /posts/{id} |
Update a post (owner only) | Yes |
| DELETE | /posts/{id} |
Delete a post (owner only) | Yes |
| POST | /vote/ |
Vote (dir: 1) or remove a vote (dir: 0) on a post |
Yes |
Authenticated requests must include Authorization: Bearer <access_token>, obtained from POST /login.
Schema changes are made in app/models.py, then a migration is generated and applied:
alembic revision --autogenerate -m "describe the change"
alembic upgrade headAlways review autogenerated migrations before applying them — Alembic cannot detect column renames automatically and will generate a destructive drop/add instead of a rename.
pip install -r requirements-dev.txt # if not already done
pytestTests run against a separate _test-suffixed database (see app/tests/conftest.py) and expect Postgres to be reachable — start it via docker compose -f docker-compose-dev.yml up -d first if it isn't running.
Defined in .github/workflows/build-deploy.yml, two jobs:
- Lint & Test — runs on every push and pull request targeting
main. Spins up a throwaway Postgres service container, installsrequirements-dev.txt, runsruff check ., thenpytest. - Build & Push Docker Image — runs only after Lint & Test passes, and only on a direct push to
main(not on PRs). Builds the image fromDockerfileand pushes it to Docker Hub, tagged both:latestand with the commit SHA.
There is no automated deploy step — the pipeline stops at publishing the image to Docker Hub, since this project doesn't run on a live server.
To run this pipeline on your own fork, configure these under Settings → Secrets and variables → Actions:
Secrets:
DATABASE_PASSWORDSECRET_KEYDOCKERHUB_TOKEN(a Docker Hub access token, not your account password)
Variables:
DATABASE_NAMEDATABASE_USERNAMEALGORITHMACCESS_TOKEN_EXPIRE_MINUTESDOCKERHUB_USERNAME