Implement all Basic, Intermediate, and Advanced level features for Phase II/V
hackathon requirements with complete CRUD operations, filtering, search, and
notification integration.
**Core Features Implemented:**
**Basic Level (Phase II - 5 core features):**
- POST /api/v1/{user_id}/tasks - Create new tasks with full field support
- GET /api/v1/{user_id}/tasks - List all tasks with nested tags
- GET /api/v1/{user_id}/tasks/{id} - Get single task details
- PUT/PATCH /api/v1/{user_id}/tasks/{id} - Full/partial task updates
- PATCH /api/v1/{user_id}/tasks/{id}/complete - Toggle completion status
- DELETE /api/v1/{user_id}/tasks/{id} - Soft delete tasks
**Intermediate Level (Phase V - Organization):**
- Tag Management: CRUD operations for user tags with color support
- Task-Tag Relationships: Many-to-many tag assignments via junction table
- Advanced Filtering: status, priority, tags (OR logic), due date ranges
- Full-Text Search: GIN-indexed search on title/description
- Dynamic Sorting: by created_at, due_date, priority, title (asc/desc)
- Untagged Filter: Special "tag=none" parameter for untagged tasks
**Advanced Level (Phase V - Intelligent Features):**
- Due Dates & Reminders: ISO 8601 timestamps with validation
- Recurring Tasks: daily/weekly/monthly patterns + custom RRULE (JSONB)
- Notification Integration: Async notification service for task events
**Technical Implementation:**
**Architecture:**
- Repository Pattern: TaskRepository, TagRepository, TaskTagRepository
- Service Layer: QueryService (dynamic filtering), NotificationService
- DTO Layer: Pydantic schemas with field validators
- Dependency Injection: JWT auth + user isolation via verify_user_match
**Data Validation:**
- Title: 1-255 chars, trimmed, non-whitespace-only
- Description: max 10K chars
- Colors: Hex validation (#RGB or #RRGGBB) with normalization
- Reminders: reminder_at < due_date constraint
- Tag Uniqueness: per-user unique constraint enforcement
**Query Optimization:**
- Dynamic SQLModel query building with selective joins
- GIN index for full-text search (tsvector on title || description)
- Composite index on (user_id, deleted_at, due_date, reminder_at)
- Eager loading of tags relationship to avoid N+1 queries
**User Isolation & Security:**
- All endpoints require JWT authentication via verify_user_match
- User ID extracted from JWT and validated against URL parameter
- Row-level security: all queries filter by user_id
- Soft deletes: deleted_at timestamp prevents unauthorized access
**Error Handling:**
- 404 for not found / wrong user / soft-deleted resources
- 409 for unique constraint violations (duplicate tag names)
- 422 for validation errors (Pydantic field validators)
- 400 for malformed requests
**Testing Infrastructure:**
- Unit Tests: Validators, query builder, repositories (mocked DB)
- Integration Tests: Full request/response cycle with test DB
- E2E Tests: Multi-user scenarios, tag assignments, complex filters
- Performance Tests: 5K+ task datasets, query timing benchmarks
**Files Created (30):**
- backend/src/api/tasks.py (330 lines) - Task CRUD endpoints
- backend/src/api/tags.py (186 lines) - Tag management endpoints
- backend/src/api/task_tags.py (151 lines) - Tag assignment endpoints
- backend/src/schemas/task.py (191 lines) - Task DTOs with validators
- backend/src/schemas/tag.py (91 lines) - Tag DTOs
- backend/src/schemas/task_tag.py (43 lines) - Task-Tag DTOs
- backend/src/schemas/common.py (22 lines) - Shared enums
- backend/src/repositories/task.py (165 lines) - Task repository
- backend/src/repositories/tag.py (123 lines) - Tag repository
- backend/src/repositories/task_tag.py (89 lines) - Task-Tag repository
- backend/src/services/query.py (158 lines) - Dynamic query builder
- backend/src/services/notification.py (67 lines) - Notification service
- backend/tests/unit/test_*.py (8 files, 450+ lines) - Unit tests
- backend/tests/integration/test_*.py (4 files, 600+ lines) - Integration tests
- backend/tests/e2e/ (2 files, 300+ lines) - End-to-end tests
- backend/tests/performance/ (1 file, 150+ lines) - Performance benchmarks
**Files Modified (13):**
- backend/main.py - Registered new API routers
- backend/src/api/deps.py - Enhanced verify_user_match dependency
- backend/src/core/config.py - Added notification settings
- backend/src/core/validators.py - Added hex color + recurrence validators
- backend/src/models/*.py (5 files) - Enhanced relationships + indexes
- backend/tests/conftest.py - Added test fixtures for integration tests
- backend/README.md - Updated API documentation
- docs/phase-2-spec-prompts.md - Updated phase planning docs
**Database Schema Updates:**
- Added GIN index on tasks(to_tsvector('english', title || ' ' || description))
- Added composite index on tasks(user_id, deleted_at, due_date, reminder_at)
- Added index on notifications(user_id, status, scheduled_for)
- Enhanced task_tags junction table with composite unique constraint
**OpenAPI Documentation:**
- All endpoints tagged and grouped (Tasks, Tags, TaskTags)
- Request/response examples for all DTOs
- Query parameter descriptions with regex patterns
- Error response documentation (404, 409, 422)
**Phase II Hackathon Requirements Met:**
✅ RESTful endpoints with /api/v1/{user_id}/* pattern
✅ JWT authentication on all endpoints
✅ User isolation (each user sees only their data)
✅ All 5 Basic Level features as web API
✅ OpenAPI/Swagger documentation auto-generated
**Phase V Hackathon Requirements Met:**
✅ All Intermediate Level features (priorities, tags, search, sort)
✅ All Advanced Level features (recurring tasks, due dates, reminders)
✅ Event-driven architecture ready (notification service hooks)
**Migration Support:**
- No schema changes required (uses existing 002-database-schema models)
- Backward compatible with Phase I console app
- Ready for Phase III AI chatbot MCP integration
**Next Steps:**
1. Run integration tests: pytest backend/tests/integration/ -v
2. Run performance benchmarks: pytest backend/tests/performance/ -v
3. Start FastAPI server: uvicorn backend.src.main:app --reload
4. View API docs: http://localhost:8000/docs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
18 KiB
Quickstart: RESTful API Endpoints
Feature: 003-api-endpoints Date: 2025-12-30 Purpose: Quick reference for implementing and testing the 15 API endpoints
Architecture Overview
┌─────────────┐
│ Client │ (Next.js frontend - Phase II)
│ (Browser) │
└──────┬──────┘
│ HTTP + JWT Bearer Token
│
┌──────▼──────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Middleware Layer │ │
│ │ - Request ID tracking │ │
│ │ - JWT validation │ │
│ │ - CORS │ │
│ └───────────────┬────────────────────────┘ │
│ │ │
│ ┌───────────────▼────────────────────────┐ │
│ │ API Routers (3) │ │
│ │ - tasks.py (7 endpoints) │ │
│ │ - tags.py (5 endpoints) │ │
│ │ - task_tags.py (3 endpoints) │ │
│ └───────────────┬────────────────────────┘ │
│ │ │
│ ┌───────────────▼────────────────────────┐ │
│ │ Dependencies │ │
│ │ - get_current_user (JWT → User) │ │
│ │ - verify_user_match (URL vs JWT) │ │
│ │ - get_session (async DB session) │ │
│ └───────────────┬────────────────────────┘ │
│ │ │
│ ┌───────────────▼────────────────────────┐ │
│ │ Repositories (3) │ │
│ │ - TaskRepository │ │
│ │ - TagRepository │ │
│ │ - TaskTagRepository │ │
│ └───────────────┬────────────────────────┘ │
│ │ │
│ ┌───────────────▼────────────────────────┐ │
│ │ Query Service │ │
│ │ - build_task_query() with filters │ │
│ │ - Full-text search │ │
│ │ - Sorting logic │ │
│ └───────────────┬────────────────────────┘ │
└──────────────────┼──────────────────────────────┘
│
┌───────────▼──────────────┐
│ Async SQLModel Session │
└───────────┬──────────────┘
│
┌───────────▼──────────────┐
│ Neon PostgreSQL │
│ - tasks table │
│ - tags table │
│ - task_tags table │
│ - GIN full-text index │
└──────────────────────────┘
Implementation Sequence
Phase 1: Foundation (Dependencies & Schemas)
Files to Create:
backend/src/schemas/common.py- Enums and ErrorResponsebackend/src/schemas/tag.py- TagCreate, TagUpdate, TagResponsebackend/src/schemas/task.py- TaskCreate, TaskUpdate, TaskReplace, TaskResponsebackend/src/schemas/task_tag.py- TaskTagCreate, TaskTagResponsebackend/src/api/deps.py- Addverify_user_matchdependency
Test:
# Validate Pydantic models
python -c "from backend.src.schemas.task import TaskCreate; print(TaskCreate.model_json_schema())"
Phase 2: Repository Layer
Files to Create:
backend/src/repositories/__init__.pybackend/src/repositories/tag.py- TagRepositorybackend/src/repositories/task.py- TaskRepositorybackend/src/repositories/task_tag.py- TaskTagRepository
Key Methods:
# TaskRepository (backend/src/repositories/task.py)
class TaskRepository:
async def create(user_id: UUID, data: TaskCreate) -> Task
async def get_by_id(user_id: UUID, task_id: int) -> Optional[Task]
async def list_tasks(user_id: UUID) -> List[Task]
async def update(user_id: UUID, task_id: int, data: TaskUpdate) -> Optional[Task]
async def replace(user_id: UUID, task_id: int, data: TaskReplace) -> Optional[Task]
async def soft_delete(user_id: UUID, task_id: int) -> bool
async def search(user_id: UUID, query: str) -> List[Task]
# TagRepository (backend/src/repositories/tag.py)
class TagRepository:
async def create(user_id: UUID, data: TagCreate) -> Tag
async def get_by_id(user_id: UUID, tag_id: int) -> Optional[Tag]
async def list_tags(user_id: UUID) -> List[Tag]
async def update(user_id: UUID, tag_id: int, data: TagUpdate) -> Optional[Tag]
async def soft_delete(user_id: UUID, tag_id: int) -> bool
async def exists_by_name(user_id: UUID, name: str) -> bool
# TaskTagRepository (backend/src/repositories/task_tag.py)
class TaskTagRepository:
async def assign_tag(user_id: UUID, task_id: int, tag_id: int) -> bool
async def unassign_tag(user_id: UUID, task_id: int, tag_id: int) -> bool
async def get_task_tags(user_id: UUID, task_id: int) -> List[Tag]
Test:
# Unit test repositories
pytest backend/tests/unit/test_repositories.py -v
Phase 3: Query Service
File to Create:
backend/src/services/query.py - QueryService with build_task_query()
Example Query Building:
# QueryService.build_task_query()
# Handles:
# - User isolation (WHERE user_id = :user_id)
# - Soft delete filter (WHERE deleted_at IS NULL)
# - Status filter (WHERE completed = :status)
# - Priority filter (WHERE priority = :priority)
# - Tag filter with OR logic (EXISTS subquery with IN clause)
# - Tag "none" filter (NOT EXISTS subquery)
# - Due date range (WHERE due_date BETWEEN :after AND :before)
# - Full-text search (WHERE to_tsvector(...) @@ plainto_tsquery(:search))
# - Sorting (ORDER BY :sort_by :order)
# - Eager loading tags (selectinload(Task.tags))
Test:
pytest backend/tests/unit/test_query_service.py -v
Phase 4: API Routers
Files to Create:
backend/src/api/tasks.py- 7 task endpointsbackend/src/api/tags.py- 5 tag endpointsbackend/src/api/task_tags.py- 3 task-tag endpoints
Endpoint Signature Pattern:
# Example: GET /api/v1/{user_id}/tasks
@router.get("/api/v1/{user_id}/tasks", response_model=List[TaskResponse])
async def list_tasks(
user: User = Depends(verify_user_match),
session: AsyncSession = Depends(get_session),
status: Optional[str] = Query(None, regex="^(incomplete|complete)$"),
priority: Optional[PriorityEnum] = None,
tag: Optional[List[str]] = Query(None),
due_before: Optional[datetime] = None,
due_after: Optional[datetime] = None,
search: Optional[str] = None,
sort: str = Query("created_at", regex="^(created_at|due_date|priority|title)$"),
order: str = Query("desc", regex="^(asc|desc)$"),
):
query_service = QueryService()
stmt = query_service.build_task_query(
user_id=user.id,
status=status,
priority=priority,
tags=tag,
due_before=due_before,
due_after=due_after,
search=search,
sort_by=sort,
order=order,
)
result = await session.execute(stmt)
tasks = result.scalars().all()
return [TaskResponse.model_validate(task) for task in tasks]
Test:
# Integration test endpoints
pytest backend/tests/integration/test_tasks.py -v
pytest backend/tests/integration/test_tags.py -v
pytest backend/tests/integration/test_task_tags.py -v
Phase 5: Register Routers in Main App
File to Modify: backend/src/main.py
from fastapi import FastAPI
from .api import tasks, tags, task_tags
app = FastAPI(
title="Todo Application API",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
)
# Register routers
app.include_router(tasks.router, tags=["Tasks"])
app.include_router(tags.router, tags=["Tags"])
app.include_router(task_tags.router, tags=["Task-Tags"])
Test:
# Run server and access OpenAPI docs
uvicorn backend.src.main:app --reload
# Open http://localhost:8000/docs
Testing Strategy
1. Unit Tests (backend/tests/unit/)
test_repositories.py:
- TaskRepository CRUD operations
- TagRepository CRUD operations
- TaskTagRepository many-to-many operations
- Soft delete behavior
- User isolation enforcement
test_query_service.py:
- Filter combinations (status + priority + tags)
- Tag "none" filter logic
- Full-text search query building
- Sorting logic
- Eager loading configuration
test_validators.py:
- Hex color validation and normalization
- reminder_at < due_date validation
- Title/name whitespace trimming
Run:
pytest backend/tests/unit/ -v --cov=backend/src/repositories --cov=backend/src/services
2. Integration Tests (backend/tests/integration/)
test_tasks.py:
- POST /api/v1/{user_id}/tasks (create)
- GET /api/v1/{user_id}/tasks (list with all filter combinations)
- GET /api/v1/{user_id}/tasks/{id} (get single)
- PUT /api/v1/{user_id}/tasks/{id} (full replacement)
- PATCH /api/v1/{user_id}/tasks/{id} (partial update)
- PATCH /api/v1/{user_id}/tasks/{id}/complete (toggle completion)
- DELETE /api/v1/{user_id}/tasks/{id} (soft delete)
test_tags.py:
- POST /api/v1/{user_id}/tags (create, test 409 on duplicate)
- GET /api/v1/{user_id}/tags (list)
- GET /api/v1/{user_id}/tags/{id} (get single)
- PUT /api/v1/{user_id}/tags/{id} (update, test hex color normalization)
- DELETE /api/v1/{user_id}/tags/{id} (soft delete)
test_task_tags.py:
- POST /api/v1/{user_id}/tasks/{id}/tags (assign, test 409 on duplicate)
- GET /api/v1/{user_id}/tasks/{id}/tags (list)
- DELETE /api/v1/{user_id}/tasks/{id}/tags/{tag_id} (unassign)
Run:
pytest backend/tests/integration/ -v --cov=backend/src/api
3. E2E Tests (backend/tests/e2e/)
test_user_isolation.py:
- User A cannot access User B's tasks (404, not 403)
- User A cannot modify User B's tasks
- User A cannot assign tags to User B's tasks
- JWT user_id mismatch returns 403
- Cross-user tag filtering returns empty results
Run:
pytest backend/tests/e2e/ -v
4. Performance Tests
Test Cases:
- Task creation: <100ms p95 (simple task)
- Task list with filters: <500ms p95 (10,000 tasks)
- Full-text search: <200ms p95 (5,000 tasks)
- N+1 query prevention: Verify eager loading with SQL logging
Run:
pytest backend/tests/performance/ -v --durations=10
Sample API Calls
1. Create Task with All Fields
curl -X POST http://localhost:8000/api/v1/123e4567-e89b-12d3-a456-426614174000/tasks \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"title": "Buy groceries",
"description": "Milk, eggs, bread",
"priority": "high",
"due_date": "2025-12-31T10:00:00Z",
"reminder_at": "2025-12-30T09:45:00Z",
"recurrence_pattern": "weekly",
"recurrence_config": {"rrule": "FREQ=WEEKLY;BYDAY=MO,FR"}
}'
Expected Response (201 Created):
{
"id": 123,
"user_id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Buy groceries",
"description": "Milk, eggs, bread",
"completed": false,
"priority": "high",
"due_date": "2025-12-31T10:00:00Z",
"reminder_at": "2025-12-30T09:45:00Z",
"recurrence_pattern": "weekly",
"recurrence_config": {"rrule": "FREQ=WEEKLY;BYDAY=MO,FR"},
"tags": [],
"created_at": "2025-12-30T14:30:00Z",
"updated_at": "2025-12-30T14:30:00Z"
}
2. List Tasks with Filters
curl -X GET "http://localhost:8000/api/v1/123e4567-e89b-12d3-a456-426614174000/tasks?status=incomplete&priority=high&tag=work&tag=urgent&sort=due_date&order=asc" \
-H "Authorization: Bearer eyJhbGc..."
Query Breakdown:
status=incomplete: Only incomplete taskspriority=high: Only high prioritytag=work&tag=urgent: Tasks with "work" OR "urgent" tagssort=due_date&order=asc: Sort by due date ascending (earliest first)
Expected Response (200 OK):
[
{
"id": 124,
"user_id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Finish report",
"completed": false,
"priority": "high",
"due_date": "2025-12-28T17:00:00Z",
"tags": [
{"id": 1, "name": "work", "color": "#FF5733", ...}
],
...
},
{
"id": 123,
"user_id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Buy groceries",
"completed": false,
"priority": "high",
"due_date": "2025-12-31T10:00:00Z",
"tags": [
{"id": 2, "name": "urgent", "color": "#FF0000", ...}
],
...
}
]
3. Full-Text Search
curl -X GET "http://localhost:8000/api/v1/123e4567-e89b-12d3-a456-426614174000/tasks?search=meeting+notes" \
-H "Authorization: Bearer eyJhbGc..."
Expected Behavior:
- PostgreSQL GIN index used (
idx_tasks_title_description_fts) - Matches tasks with "meeting" OR "notes" in title/description
- Stemming applied (e.g., "meeting" matches "meetings")
- Results returned in <200ms
4. Create Tag with Color Normalization
curl -X POST http://localhost:8000/api/v1/123e4567-e89b-12d3-a456-426614174000/tags \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{"name": "work", "color": "#f5a"}'
Expected Response (201 Created):
{
"id": 1,
"user_id": "123e4567-e89b-12d3-a456-426614174000",
"name": "work",
"color": "#FF55AA", // Normalized from #f5a to #FF55AA
"created_at": "2025-12-30T14:30:00Z"
}
5. Assign Tag to Task
curl -X POST http://localhost:8000/api/v1/123e4567-e89b-12d3-a456-426614174000/tasks/123/tags \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{"tag_id": 1}'
Expected Response (201 Created):
{
"task_id": 123,
"tag_id": 1,
"message": "Tag assigned successfully"
}
6. Partial Update Task (PATCH)
curl -X PATCH http://localhost:8000/api/v1/123e4567-e89b-12d3-a456-426614174000/tasks/123 \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries and bread", "priority": "medium"}'
Expected Behavior:
- Only
titleandpriorityupdated description,due_date, etc. remain unchangedupdated_attimestamp automatically updated
Error Handling Examples
1. Duplicate Tag Name (409 Conflict)
# Create tag "work"
curl -X POST .../tags -d '{"name": "work", "color": "#FF5733"}'
# Try to create another tag "work" (same user)
curl -X POST .../tags -d '{"name": "work", "color": "#00FF00"}'
Expected Response (409 Conflict):
{
"error": "Tag name already exists",
"code": "TAG_NAME_CONFLICT",
"status": 409,
"request_id": "req_abc123"
}
2. User ID Mismatch (403 Forbidden)
# JWT contains user_id = 123e4567...
# But URL has different user_id = 999e8888...
curl -X GET http://localhost:8000/api/v1/999e8888-e89b-12d3-a456-426614174000/tasks \
-H "Authorization: Bearer eyJhbGc..." # JWT for user 123e4567...
Expected Response (403 Forbidden):
{
"error": "User ID mismatch",
"code": "FORBIDDEN",
"status": 403,
"request_id": "req_abc123"
}
3. Cross-User Task Access (404 Not Found)
# User A tries to access User B's task
curl -X GET http://localhost:8000/api/v1/123e4567.../tasks/999 \
-H "Authorization: Bearer ..." # JWT for user 123e4567...
# Task 999 belongs to a different user
Expected Response (404 Not Found - NOT 403):
{
"error": "Task not found",
"code": "TASK_NOT_FOUND",
"status": 404,
"request_id": "req_abc123"
}
Rationale: Return 404 instead of 403 to prevent user enumeration attacks (attacker cannot tell if task exists but is forbidden vs doesn't exist)
4. Invalid Hex Color (422 Validation Error)
curl -X POST .../tags -d '{"name": "work", "color": "#GGGGGG"}'
Expected Response (422 Unprocessable Entity):
{
"error": "Invalid hex color format. Use #RRGGBB or #RGB.",
"code": "VALIDATION_ERROR",
"status": 422,
"request_id": "req_abc123"
}
Performance Checklist
Before marking feature complete, verify:
- Task creation <100ms p95 (simple tasks)
- Task list with filters <500ms p95 (10,000 tasks)
- Full-text search <200ms p95 (5,000 tasks)
- No N+1 queries (verify with SQL logging: only 2 queries for task + tags)
- Soft delete filter applied on all queries
- User isolation enforced on all endpoints
- Connection pool sized 5-20 (check
backend/src/core/database.py) - GIN index used for full-text search (verify with EXPLAIN ANALYZE)
Deployment Checklist
Before deploying to production:
- All 50+ tests passing
- OpenAPI docs generated at /docs
- Environment variables set (DATABASE_URL, JWT_SECRET)
- CORS configured for frontend domain
- Health check endpoint /health returning 200
- Request ID middleware active (for tracing)
- Database migrations applied (no changes needed for this spec)
- Error responses follow standard format {error, code, status, request_id}
- Soft delete filtering tested (deleted tasks excluded from queries)
Next Step: Run /sp.tasks to generate atomic implementation tasks from this plan