I developed this backend service as a RESTful API to power a Kanban board application. The implementation showcases modern Python development practices and industry-standard architectural patterns.
| Component | Choice | Justification |
|---|---|---|
| Framework | FastAPI | Fast to develop with, automatic OpenAPI docs, built-in request/response validation |
| ORM | SQLAlchemy | Familiar with it, great async support, type hints in v2.0 speed up development |
| Production Database | PostgreSQL | Production ready, good async driver support with asyncpg |
| Development Database | SQLite | Simple to set up, perfect for local development |
| Testing Framework | Pytest | Simple to write tests, great fixture system |
| Task Automation | Nox | Simple to set up, isolates task environments |
| Package Management | Poetry | Modern tool, handles dependencies well, clear dependency specification |
| Code Quality | Ruff+Black | Fast linting and formatting, good defaults |
The backend follows a modular structure for maintainability and separation of concerns:
kanban_api/
├── config.py # Environment and app configuration
├── database.py # Database connection and session management
├── dependencies.py # FastAPI dependency injection (auth, current user)
├── models/ # SQLAlchemy ORM models
│ ├── board.py # Board model and relationships
│ ├── card.py # Card model and relationships
│ └── user.py # User model and auth-related methods
├── routes/ # API endpoints grouped by resource
│ ├── auth.py # Authentication endpoints
│ ├── boards.py # Board management
│ ├── cards.py # Card operations
│ └── users.py # User management
├── schemas/ # Pydantic models for validation
│ ├── board.py # Board request/response schemas
│ ├── card.py # Card request/response schemas
│ └── user.py # User request/response schemas
└── utils/ # Shared utilities
├── password.py # Password hashing
└── token.py # JWT token handling
erDiagram
User {
int id PK
string email UK
string hashed_password
datetime created_at
datetime updated_at
}
Board {
int id PK
string title
string description
datetime created_at
datetime updated_at
}
Card {
int id PK
string title
enum column
string description
int board_id FK
datetime created_at
datetime updated_at
}
Label {
int id PK
int board_id FK
enum color
string name
datetime created_at
datetime updated_at
}
UserBoard {
int user_id PK,FK
int board_id PK,FK
datetime created_at
datetime updated_at
}
CardLabel {
int card_id PK,FK
int label_id PK,FK
datetime created_at
datetime updated_at
}
User ||--o{ UserBoard : ""
Board ||--o{ UserBoard : ""
Board ||--o{ Card : ""
Board ||--o{ Label : ""
Card ||--o{ CardLabel : ""
Label ||--o{ CardLabel : ""
I implemented a security system focusing on:
Here’s how the authentication flow works:
# Using industry-standard bcrypt for password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
# Tokens expire after 1 hour and contain user ID and email
@router.post("/auth/tokens", status_code=200)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)) -> Token:
# Validate credentials
user = db.execute(select(User).where(User.email == form_data.username)).scalar_one_or_none()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(status_code=401)
# Generate JWT token
token_data = TokenData(sub=str(user.id), email=user.email)
access_token = create_access_token(
token_data,
expires_delta=timedelta(hours=1)
)
return Token(access_token=access_token, token_type="bearer")
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> User:
try:
# Decode and validate token
token_data = decode_access_token(token)
if not token_data:
raise InvalidTokenError
# Get user from database
user = db.execute(
select(User).where(User.id == int(token_data.sub))
).scalar_one_or_none()
if not user:
raise InvalidTokenError
return user
except InvalidTokenError:
raise HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
I chose SQLAlchemy 2.0 because its modern style provides clear type hints and makes database interactions more straightforward:
class Board(Base, TimestampMixin):
__tablename__ = "board"
id: Mapped[int] = mapped_column(Identity(), primary_key=True)
title: Mapped[str] = mapped_column(nullable=False)
description: Mapped[str] = mapped_column(nullable=True)
# Relationships with cascading deletes
cards: Mapped[list["Card"]] = relationship(
cascade="all, delete-orphan",
back_populates="board"
)
owners: Mapped[list["User"]] = relationship(
secondary="users_boards",
back_populates="boards"
)
The API endpoints were designed with the following goals:
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/tokens | Login |
| POST | /api/users | Create user |
| GET | /api/users/me | Get current user |
| PATCH | /api/users/me | Update current user |
| POST | /api/users/search | Search user by email |
| GET | /api/boards | List boards |
| POST | /api/boards | Create board |
| GET | /api/boards/{board_id} | Get board |
| DELETE | /api/boards/{board_id} | Delete board |
| PATCH | /api/boards/{board_id} | Update board |
| POST | /api/boards/{board_id}/cards | Create cards |
| GET | /api/boards/{board_id}/cards | List cards |
| PUT | /api/boards/{board_id}/users/{user_id} | Share board |
| DELETE | /api/boards/{board_id}/users/{user_id} | Unshare board |
| GET | /api/boards/{board_id}/users | List board users |
| GET | /api/cards/{card_id} | Get card |
| PATCH | /api/cards/{card_id} | Update card |
| DELETE | /api/cards/{card_id} | Delete card |
I implemented a comprehensive testing approach:
@pytest.fixture(scope="session")
async def auth_client(client: AsyncClient) -> AsyncClient:
"""Provides an authenticated client for testing protected endpoints."""
user_data = {"email": "test@example.com", "password": "testpass123"}
await client.post("/api/users", json=user_data)
response = await client.post("/api/auth/tokens", data=user_data)
token = response.json()["access_token"]
client.headers["Authorization"] = f"Bearer {token}"
return client
async def test_create_board(auth_client: AsyncClient):
"""Test board creation with authentication."""
response = await auth_client.post(
"/api/boards",
json={"title": "Test Board", "description": "Test Description"}
)
assert response.status_code == 201
data = response.json()
assert data["title"] == "Test Board"
The project uses GitHub Actions for automation:
CI Pipeline
CD Pipeline
When working with SQLAlchemy, I quickly realized that understanding the ORM’s internals was crucial for efficient database operations. Simply using the ORM as a black box wasn’t enough.
I spent significant time reading documentation and debugging SQL queries to truly understand how SQLAlchemy translates Python code into database operations. This deep dive helped me:
Testing presented unexpected challenges with Python’s import caching and fixture scoping. I discovered that module imports can be cached across test sessions, which means environment variables and configurations might not reset as expected.
To solve this, I:
__init__.pyI initially attempted to implement an async API, but the complexity and refactoring effort quickly became prohibitive. The development cost of maintaining a fully async implementation outweighed the potential performance benefits.
After careful consideration, I decided to use a synchronous approach, which simplified the codebase and made the implementation more maintainable.
Based on the current implementation, here are planned enhancements:
API Enhancements
Security Enhancements
Database Improvements
Auto-generated API documentation showing available endpoints