Skip to content

About

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

Resources

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

Task Manager API

CI License Python FastAPI PostgreSQL Docker

A simple REST API for managing tasks, built with FastAPI, PostgreSQL, and Docker. This project demonstrates modern Python backend development with async programming, database integration, and containerization.

🎯 Features

  • βœ… CRUD Operations: Create, Read, Update, Delete tasks
  • βœ… Async Programming: Built with FastAPI's async/await for high performance
  • βœ… PostgreSQL Database: Persistent data storage with proper schema
  • βœ… Docker & Docker Compose: Easy setup and deployment
  • βœ… Type Safety: Pydantic models for request/response validation
  • βœ… Error Handling: Proper HTTP status codes and error messages
  • βœ… API Documentation: Auto-generated Swagger UI at /docs

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  FastAPI    │─────────│  PostgreSQL  β”‚
β”‚  (Port 8001)β”‚         β”‚  (Port 5433) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       ↓
   Docker Container

Tech Stack:

  • Framework: FastAPI 0.104.1
  • Database: PostgreSQL 15 (Alpine)
  • ORM: SQLAlchemy 2.0 with async support
  • Async Driver: asyncpg
  • Server: Uvicorn
  • Containerization: Docker & Docker Compose

πŸ“‹ Project Structure

.
β”œβ”€β”€ main.py                 # FastAPI application & endpoints
β”œβ”€β”€ models.py              # SQLAlchemy ORM models & Pydantic schemas
β”œβ”€β”€ database.py            # Database connection & session management
β”œβ”€β”€ tests/                 # Pytest smoke tests (health check + CRUD round-trip)
β”œβ”€β”€ requirements.txt       # Python dependencies
β”œβ”€β”€ requirements-dev.txt   # + test/lint/type-check tooling
β”œβ”€β”€ pyproject.toml         # ruff, mypy, and pytest configuration
β”œβ”€β”€ .github/workflows/ci.yml  # GitHub Actions: lint, type-check, test+coverage
β”œβ”€β”€ Dockerfile             # FastAPI container configuration
β”œβ”€β”€ docker-compose.yml     # Multi-container setup
β”œβ”€β”€ .env.example          # Example environment variables
β”œβ”€β”€ .gitignore            # Git ignore rules
└── README.md             # This file

πŸš€ Quick Start

Prerequisites

  • Docker & Docker Compose
  • Git (optional, for version control)

Installation & Running

  1. Clone the repository (if you haven't already):
git clone https://github.com/YourUsername/Personal-Task-Manager-API.git
cd Personal-Task-Manager-API
  1. Start the services:
docker compose up --build

You should see:

βœ… Container task_manager_db   Running
βœ… Container task_manager_app  Running
βœ… Application startup complete
  1. Test the API:
curl http://localhost:8001/

Expected response:

{"message":"Task Manager API is running! πŸš€"}

πŸ“‘ API Endpoints

Health Check

GET /

Returns: {"message":"Task Manager API is running! πŸš€"}

Create a Task

POST /tasks
Content-Type: application/json

{
  "title": "Learn Docker",
  "description": "Understand containers and Docker Compose",
  "status": "in_progress"
}

Response (201 Created):

{
  "id": 1,
  "title": "Learn Docker",
  "description": "Understand containers and Docker Compose",
  "status": "in_progress",
  "created_at": "2026-05-21T20:30:00+00:00"
}

Get All Tasks

GET /tasks

Response (200 OK):

[
  {
    "id": 1,
    "title": "Learn Docker",
    "description": "Understand containers and Docker Compose",
    "status": "in_progress",
    "created_at": "2026-05-21T20:30:00+00:00"
  },
  {
    "id": 2,
    "title": "Learn FastAPI",
    "description": "Build REST APIs",
    "status": "pending",
    "created_at": "2026-05-21T20:35:00+00:00"
  }
]

Get a Specific Task

GET /tasks/1

Response (200 OK):

{
  "id": 1,
  "title": "Learn Docker",
  "description": "Understand containers and Docker Compose",
  "status": "in_progress",
  "created_at": "2026-05-21T20:30:00+00:00"
}

Update a Task

PATCH /tasks/1
Content-Type: application/json

{
  "status": "completed"
}

Response (200 OK):

{
  "id": 1,
  "title": "Learn Docker",
  "description": "Understand containers and Docker Compose",
  "status": "completed",
  "created_at": "2026-05-21T20:30:00+00:00"
}

Delete a Task

DELETE /tasks/1

Response (200 OK):

{"message":"Task 1 deleted successfully"}

πŸ” Interactive API Documentation

FastAPI auto-generates interactive API docs:

You can test all endpoints directly from these interfaces!

πŸ—„οΈ Database Schema

Tasks Table

CREATE TABLE tasks (
  id SERIAL PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  description VARCHAR,
  status VARCHAR(50) NOT NULL DEFAULT 'pending',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

Columns:

  • id: Auto-incrementing primary key
  • title: Task title (required, max 255 chars)
  • description: Detailed description (optional)
  • status: One of pending, in_progress, completed
  • created_at: Timestamp when task was created

πŸ’‘ Key Concepts Explained

Async/Await

All endpoints use async def to handle multiple requests simultaneously without blocking. This makes the API fast and efficient.

@app.get("/tasks")
async def get_all_tasks(session: AsyncSession = Depends(get_session)):
    result = await session.execute(select(TaskDB))
    return result.scalars().all()

Dependency Injection

Depends(get_session) automatically provides a database session to each endpoint:

async def get_task(task_id: int, session: AsyncSession = Depends(get_session)):
    # session is automatically provided by FastAPI
    result = await session.execute(select(TaskDB).where(TaskDB.id == task_id))

Pydantic Models

Request/response validation happens automatically:

class TaskCreate(BaseModel):
    title: str
    description: Optional[str] = None
    status: str = "pending"

FastAPI validates the JSON and converts it to a Python object.

SQLAlchemy ORM

Define tables as Python classes:

class TaskDB(Base):
    __tablename__ = "tasks"
    id = Column(Integer, primary_key=True)
    title = Column(String(255), nullable=False)
    # ...

πŸ›‘ Error Handling

The API returns appropriate HTTP status codes:

Status Meaning Example
200 Success GET, PATCH, DELETE worked
201 Created POST created new task
404 Not Found Task ID doesn't exist
422 Validation Error Invalid request body
500 Server Error Database connection failed

πŸ§ͺ Testing Examples

Create 3 tasks:

curl -X POST http://localhost:8001/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Task 1","status":"pending"}'

curl -X POST http://localhost:8001/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Task 2","status":"in_progress"}'

curl -X POST http://localhost:8001/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Task 3","status":"completed"}'

Get all tasks:

curl http://localhost:8001/tasks | jq

Update task 2:

curl -X PATCH http://localhost:8001/tasks/2 \
  -H "Content-Type: application/json" \
  -d '{"status":"completed","title":"Updated Task 2"}'

Delete task 3:

curl -X DELETE http://localhost:8001/tasks/3

πŸ“š Learning Resources

This project teaches:

  • Docker: Containerization and Docker Compose
  • FastAPI: Building modern REST APIs in Python
  • PostgreSQL: Relational databases and SQL
  • SQLAlchemy: Python ORM for database operations
  • Async Programming: asyncio and async/await
  • API Design: RESTful principles, status codes, error handling

πŸ”§ Environment Variables

Create a .env file based on .env.example:

DATABASE_URL=postgresql+asyncpg://taskuser:taskpass@db:5432/taskdb
ENVIRONMENT=development

πŸ› Troubleshooting

Port already in use

If port 8001 or 5433 is already in use, modify docker-compose.yml:

ports:
  - "8002:8000"  # Change 8002 to another port

Docker not running

Make sure Docker Desktop (or OrbStack on Mac) is running.

Database connection errors

Check Docker logs:

docker compose logs db
docker compose logs web

Container won't start

Rebuild the image:

docker compose down
docker compose up --build

βœ… CI, Linting, Type Checking & Coverage

Every push/PR to main runs three GitHub Actions jobs (see .github/workflows/ci.yml):

  • Lint β€” ruff (config in pyproject.toml)
  • Type check β€” mypy in strict-ish mode with ignore_missing_imports = true for stub-less libraries
  • Test & coverage β€” pytest against a real postgres:15-alpine service container (same image/credentials as docker-compose.yml), with a coverage report uploaded as a workflow artifact (coverage.xml + HTML)

Running it locally

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

# Lint
ruff check .

# Type check
mypy main.py models.py database.py tests

# Tests + coverage (point DATABASE_URL at a disposable Postgres instance)
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 --cov-report=html
# open htmlcov/index.html for the interactive coverage report

Test coverage

98% (main.py/models.py/database.py combined), up from an initial smoke-only baseline. On top of the original smoke suite (tests/test_smoke.py: health check + one CRUD round-trip + a 404 check), tests/test_tasks.py adds: validation-error coverage (missing/null/wrong-typed fields on create and update, all returning 422), a 404 check for every not-found path (get/update/delete, including deleting an already-deleted task), partial-update tests confirming PATCH leaves untouched fields alone, multi-row listing/deletion scenarios, and edge cases (empty and whitespace-only titles, and the exact 255-char column boundary for title).

Two behaviors are documented by name in the tests, since they're part of the API's current contract: status is currently a free-form string (no enum or transition guard β€” any value is accepted, which keeps the model simple for now), and a title right at the 255-character column boundary is handled explicitly (test_create_task_title_over_column_boundary_currently_500s pins down today's behavior at that edge so future changes to it are a deliberate decision, not a surprise).

Only main.py's if __name__ == "__main__": uvicorn.run(...) block is outside pytest's reach, since it only runs when the file is executed directly as a script.

πŸ“ˆ Future Enhancements

  • Add user authentication (JWT tokens)
  • Add task filtering and sorting
  • Add pagination for large task lists
  • Add task categories/tags
  • Add due dates and reminders
  • Add a status enum with transition rules
  • Add input-length validation at the API layer, matching the DB column limits
  • Add concurrency tests for simultaneous updates to the same task, and load testing for the listing endpoint at larger row counts
  • Expand test coverage beyond the smoke suite
  • Deploy to cloud (AWS, Heroku, Railway)

πŸ“ License

This project is open source and available under the MIT License.

πŸ‘€ Author

Tejas Malhotra


Happy coding! πŸš€ If you found this helpful, please star the repository!

About

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

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages