π Day 7: Project β Mini To-Do API β
Project Overview β
Today we'll build a complete To-Do API with the following features:
- CRUD operations for tasks
- JSON data storage
- Basic authentication
- Request validation
- Error handling middleware
Project Structure β
todo-api/
βββ app/
β βββ __init__.py
β βββ main.py
β βββ models.py
β βββ routes.py
β βββ middleware.py
β βββ storage.py
βββ .env
βββ requirements.txt
Implementation β
1. Models (models.py) β
python
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from uuid import UUID, uuid4
class Task(BaseModel):
id: UUID = Field(default_factory=uuid4)
title: str
description: Optional[str] = None
completed: bool = False
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: Optional[datetime] = None
class TaskCreate(BaseModel):
title: str
description: Optional[str] = None
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
2. Storage (storage.py) β
python
import json
from typing import Dict, List, Optional
from uuid import UUID
from .models import Task
import os
class JSONStorage:
def __init__(self, file_path: str = "tasks.json"):
self.file_path = file_path
self.tasks: Dict[str, Task] = {}
self.load()
def load(self) -> None:
if os.path.exists(self.file_path):
with open(self.file_path, "r") as f:
data = json.load(f)
self.tasks = {
k: Task(**v) for k, v in data.items()
}
def save(self) -> None:
with open(self.file_path, "w") as f:
json.dump(
{k: v.dict() for k, v in self.tasks.items()},
f,
default=str,
indent=2
)
def get_all(self) -> List[Task]:
return list(self.tasks.values())
def get_by_id(self, task_id: UUID) -> Optional[Task]:
return self.tasks.get(str(task_id))
def create(self, task: Task) -> Task:
self.tasks[str(task.id)] = task
self.save()
return task
def update(self, task_id: UUID, task: Task) -> Optional[Task]:
if str(task_id) in self.tasks:
self.tasks[str(task_id)] = task
self.save()
return task
return None
def delete(self, task_id: UUID) -> bool:
if str(task_id) in self.tasks:
del self.tasks[str(task_id)]
self.save()
return True
return False
3. Middleware (middleware.py) β
python
from nexios import get_application
from nexios.http import Request, Response
from nexios.types import Middleware
import time
async def timing_middleware(
request: Request,
response: Response,
call_next: Middleware
) -> Response:
start_time = time.time()
response = await call_next()
process_time = time.time() - start_time
response.set_header("X-Process-Time",str(process_time))
return response
async def error_middleware(
request: Request,
response: Response,
call_next: Middleware
) -> Response:
try:
return await call_next()
except Exception as e:
return response.json(
content={
"error": str(e),
"type": e.__class__.__name__
},
status_code=500
)
4. Routes (routes.py) β
python
from nexios import Router
from nexios.http import Response
from nexios import status
from .models import Task, TaskCreate, TaskUpdate
from .storage import JSONStorage
from datetime import datetime
from uuid import UUID
router = Router(prefix="/api/tasks")
storage = JSONStorage()
@router.get("/")
async def list_tasks(request: Request, response: Response):
tasks = storage.get_all()
return {
"total": len(tasks),
"tasks": tasks
}
@router.post("/")
async def create_task(request: Request, response: Response):
data = await req.json
task = Task(
**data
)
created_task = storage.create(task)
return response.json(
content=created_task.dict(),
status_code=status.HTTP_201_CREATED
)
@router.get("/{task_id}")
async def get_task(request: Request, response: Response,task_id: UUID):
task = storage.get_by_id(task_id)
if not task:
return response.json(
content={"error": "Task not found"},
status_code=status.HTTP_404_NOT_FOUND
)
return task
@router.put("/{task_id}")
async def update_task(task_id: UUID, data: TaskUpdate):
task = storage.get_by_id(task_id)
if not task:
return response.json(
content={"error": "Task not found"},
status_code=status.HTTP_404_NOT_FOUND
)
# Update fields
if data.title is not None:
task.title = data.title
if data.description is not None:
task.description = data.description
if data.completed is not None:
task.completed = data.completed
task.updated_at = datetime.utcnow()
updated_task = storage.update(task_id, task)
return updated_task
@router.delete("/{task_id}")
async def delete_task(task_id: UUID):
if storage.delete(task_id):
return Response(status_code=status.HTTP_204_NO_CONTENT)
return Response(
content={"error": "Task not found"},
status_code=status.HTTP_404_NOT_FOUND
)
5. Main Application (main.py) β
python
from nexios import get_application
from .routes import router
from .middleware import timing_middleware, error_middleware
app = get_application()
# Add middleware
app.add_middleware(timing_middleware)
app.add_middleware(error_middleware)
# Include routes
app.include_router(router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=True
)
Testing the API β
Using curl β
bash
# List tasks
curl http://localhost:8000/api/tasks
# Create task
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn Nexios", "description": "Complete the course"}'
# Get task
curl http://localhost:8000/api/tasks/{task_id}
# Update task
curl -X PUT http://localhost:8000/api/tasks/{task_id} \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Delete task
curl -X DELETE http://localhost:8000/api/tasks/{task_id}
π Practice Exercise β
Extend the To-Do API with:
Task Categories:
- Add category field to tasks
- Filter tasks by category
- Category statistics
Due Dates:
- Add due_date field
- Overdue task detection
- Task reminders
Task Priority:
- Add priority levels
- Sort by priority
- Priority-based filtering
π― Next Steps β
Next week in Day 8: JWT Auth (Part 1), we'll explore:
- JWT authentication basics
- Token creation and verification
- Protected endpoints