Skip to content

About

Async REST API for managing tasks, built with FastAPI, PostgreSQL, and Docker.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Repository files navigation

Personal Task Manager

CI License Python FastAPI React PostgreSQL Docker

A full-stack task manager: an async FastAPI/PostgreSQL REST API underneath, a React + Tailwind UI (filterable task list, stat cards, create/edit form) on top. SQLAlchemy's async engine talks to Postgres over asyncpg, so a request never blocks the event loop while waiting on the database. The whole backend stack (API + Postgres) runs with one docker compose up command, and the project ships with a pytest suite that runs against a real Postgres instance in CI.

Request flow

sequenceDiagram
    participant Client
    participant FastAPI as FastAPI (main.py)
    participant Session as AsyncSession (database.py)
    participant Postgres

    Client->>FastAPI: HTTP request (e.g. POST /tasks)
    FastAPI->>FastAPI: Validate body with Pydantic (TaskCreate/TaskUpdate)
    FastAPI->>Session: Depends(get_session)
    Session->>Postgres: await session.execute(...) via asyncpg
    Postgres-->>Session: rows
    Session-->>FastAPI: ORM objects (TaskDB)
    FastAPI-->>Client: JSON response (TaskResponse)
Loading

Features

Backend

  • Full CRUD on tasks: POST, GET (list and by id), PATCH, DELETE
  • Fully async request path: FastAPI route handlers, SQLAlchemy's AsyncSession, and the asyncpg driver, no blocking database calls
  • Request and response validation with Pydantic models (TaskCreate, TaskUpdate, TaskResponse), separate from the SQLAlchemy ORM model (TaskDB)
  • Partial updates: PATCH /tasks/{id} only touches the fields you send, via model_dump(exclude_unset=True)
  • Proper error handling (404 for missing tasks, 422 for validation errors)
  • Tables created automatically on startup through a FastAPI lifespan hook
  • One-command local stack with Docker Compose (API container + Postgres container with a health check gate)
  • CI pipeline (GitHub Actions) running ruff, mypy, and pytest with coverage against a real Postgres service container

Frontend

  • React + Tailwind single-page UI talking directly to the API (frontend/src/api.js)
  • Filterable task list (all / pending / in progress / done) with live stat cards for each status
  • Create and edit tasks through a form component, delete with confirmation
  • Graceful error state when the backend is unreachable, instead of a blank screen

Tech stack

Layer Choice
Backend framework FastAPI
Backend language Python 3.11
ORM SQLAlchemy 2.0 (async)
Database driver asyncpg
Database PostgreSQL 15
Server Uvicorn
Validation Pydantic 2
Frontend React 18, Tailwind CSS, Vite
Containerization Docker, Docker Compose (API, Postgres, and frontend each have a Dockerfile)
Testing pytest, pytest-asyncio, coverage.py
Linting / typing ruff, mypy
CI GitHub Actions

Data model

The tasks table (models.py):

Column Type Notes
id integer primary key, auto-increment
title varchar(255) required
description text optional
status varchar(50) free-form string, defaults to pending
created_at timestamptz set by the database on insert

Running it locally

With Docker (recommended)

git clone https://github.com/ApparentlyTejas/Personal-Task-Manager-API.git
cd Personal-Task-Manager-API
docker compose up --build

This starts two containers: task_manager_db (Postgres, host port 5433) and task_manager_app (FastAPI, host port 8001). The app container waits for Postgres's health check before starting.

curl http://localhost:8001/
# {"message":"Task Manager API is running! 🚀"}

Without Docker

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# Point at your own Postgres instance
export DATABASE_URL="postgresql+asyncpg://taskuser:taskpass@localhost:5432/taskdb"

uvicorn main:app --reload

Interactive docs are available at /docs (Swagger) and /redoc once the server is running.

Frontend

docker-compose.yml only starts the API and Postgres; run the React UI separately (it has its own Dockerfile for a containerized build, but npm run dev is simplest for local use):

cd frontend
npm install
npm run dev

Opens at http://localhost:5173. It talks to the API at http://localhost:8000 by default (VITE_API_URL in frontend/.env, see .env.example), which matches the "Without Docker" backend above; if you're running the API via docker compose instead (host port 8001), set VITE_API_URL=http://localhost:8001.

Example request

curl -X POST http://localhost:8001/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Write project README", "description": "Document the API for recruiters", "status": "in_progress"}'

Response:

{
  "id": 1,
  "title": "Write project README",
  "description": "Document the API for recruiters",
  "status": "in_progress",
  "created_at": "2026-05-21T20:30:00+00:00"
}

Fetching it back:

curl http://localhost:8001/tasks/1
{
  "id": 1,
  "title": "Write project README",
  "description": "Document the API for recruiters",
  "status": "in_progress",
  "created_at": "2026-05-21T20:30:00+00:00"
}

Updating just the status, leaving everything else untouched:

curl -X PATCH http://localhost:8001/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'

Testing

The test suite (tests/test_smoke.py and tests/test_tasks.py) runs with pytest against a real Postgres database, not a mock. It covers the health check, a full CRUD round-trip, 404s on every not-found path, 422s on validation errors (missing, null, or wrong-typed fields), partial-update behavior (confirming PATCH leaves unset fields alone), multi-row listing and deletion, and edge cases like empty titles and the 255-character column boundary on title. Coverage sits at 98% across main.py, models.py, and database.py.

To run it locally:

pip install -r requirements-dev.txt

docker run -d --name taskdb-test -e POSTGRES_USER=taskuser -e POSTGRES_PASSWORD=taskpass \
  -e POSTGRES_DB=taskdb -p 5544:5432 postgres:15-alpine

DATABASE_URL="postgresql+asyncpg://taskuser:taskpass@localhost:5544/taskdb" \
  pytest --cov=main --cov=models --cov=database --cov-report=term-missing

Every push and pull request to main also runs ruff and mypy in CI, alongside the test job, see .github/workflows/ci.yml.

Possible next steps

  • Authentication (JWT)
  • Filtering, sorting, and pagination on GET /tasks
  • A proper status enum with transition rules, instead of a free-form string
  • Deployment to a cloud provider

License

MIT. See LICENSE.

Author

Tejas Malhotra GitHub: ApparentlyTejas

About

Async REST API for managing tasks, built with FastAPI, PostgreSQL, and Docker.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages