Tutorial: Build a Task API
Learn myfy by building a complete REST API for task management.
What you'll build: - Task CRUD operations - Type-safe configuration - Dependency injection - Request validation - In-memory database
Time: 15-20 minutes
Step 1: Project Setup
Create a new directory:
Step 2: Define Your Models
Create models.py:
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field
class TaskStatus(str, Enum):
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
class Task(BaseModel):
id: int
title: str
description: str | None = None
status: TaskStatus = TaskStatus.TODO
created_at: datetime = Field(default_factory=datetime.now)
class CreateTaskDTO(BaseModel):
title: str = Field(min_length=1, max_length=200)
description: str | None = None
class UpdateTaskDTO(BaseModel):
title: str | None = Field(None, min_length=1, max_length=200)
description: str | None = None
status: TaskStatus | None = None
Why Pydantic?
Pydantic provides runtime validation and serialization out of the box. myfy embraces Pydantic throughout.
Step 3: Create a Repository
Create repository.py:
from models import Task, TaskStatus
class TaskRepository:
"""In-memory task storage."""
def __init__(self):
self._tasks: dict[int, Task] = {}
self._next_id = 1
def create(self, title: str, description: str | None = None) -> Task:
task = Task(
id=self._next_id,
title=title,
description=description,
status=TaskStatus.TODO,
)
self._tasks[task.id] = task
self._next_id += 1
return task
def get(self, task_id: int) -> Task | None:
return self._tasks.get(task_id)
def list_all(self) -> list[Task]:
return list(self._tasks.values())
def update(self, task_id: int, **updates) -> Task | None:
task = self._tasks.get(task_id)
if not task:
return None
# Update fields
for field, value in updates.items():
if value is not None and hasattr(task, field):
setattr(task, field, value)
return task
def delete(self, task_id: int) -> bool:
return self._tasks.pop(task_id, None) is not None
Step 4: Configure Dependency Injection
Create config.py:
from myfy.core import BaseSettings, provider, SINGLETON
from pydantic import Field
from repository import TaskRepository
class AppSettings(BaseSettings):
app_name: str = Field(default="Task API")
max_tasks: int = Field(default=1000)
@provider(scope=SINGLETON)
def task_repository() -> TaskRepository:
"""Provide task repository as singleton."""
return TaskRepository()
Singleton Scope
SINGLETON means one instance for the entire application. Perfect for repositories and databases.
Step 5: Create HTTP Routes
Create routes.py:
from myfy.web import route
from models import Task, CreateTaskDTO, UpdateTaskDTO
from repository import TaskRepository
@route.get("/tasks")
async def list_tasks(repo: TaskRepository) -> list[Task]:
"""List all tasks."""
return repo.list_all()
@route.get("/tasks/{task_id}")
async def get_task(task_id: int, repo: TaskRepository) -> Task:
"""Get a single task by ID."""
task = repo.get(task_id)
if not task:
from starlette.exceptions import HTTPException
raise HTTPException(status_code=404, detail="Task not found")
return task
@route.post("/tasks", status_code=201)
async def create_task(body: CreateTaskDTO, repo: TaskRepository) -> Task:
"""Create a new task."""
return repo.create(title=body.title, description=body.description)
@route.patch("/tasks/{task_id}")
async def update_task(
task_id: int,
body: UpdateTaskDTO,
repo: TaskRepository,
) -> Task:
"""Update an existing task."""
updates = body.model_dump(exclude_unset=True)
task = repo.update(task_id, **updates)
if not task:
from starlette.exceptions import HTTPException
raise HTTPException(status_code=404, detail="Task not found")
return task
@route.delete("/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int, repo: TaskRepository) -> None:
"""Delete a task."""
if not repo.delete(task_id):
from starlette.exceptions import HTTPException
raise HTTPException(status_code=404, detail="Task not found")
Automatic DI
Notice how repo: TaskRepository is automatically injected? myfy resolves dependencies at compile-time.
Step 6: Wire It All Together
Create app.py:
from myfy.core import Application
from myfy.web import WebModule
from config import AppSettings
# Create application
app = Application(settings_class=AppSettings, auto_discover=False)
app.add_module(WebModule())
if __name__ == "__main__":
import asyncio
asyncio.run(app.run())
Step 7: Create Environment Config
Create .env:
Step 8: Run Your API
You should see:
๐ Starting myfy development server...
โ Found application in app.py
๐ก Listening on http://127.0.0.1:8000
๐ฆ Loaded 2 module(s)
๐ Reload enabled - watching for file changes
Step 9: Test Your API
Create a Task
curl -X POST http://127.0.0.1:8000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn myfy", "description": "Complete the tutorial"}'
Response:
{
"id": 1,
"title": "Learn myfy",
"description": "Complete the tutorial",
"status": "todo",
"created_at": "2025-10-26T10:30:00"
}
List All Tasks
Get Single Task
Update Task
curl -X PATCH http://127.0.0.1:8000/tasks/1 \
-H "Content-Type: application/json" \
-d '{"status": "done"}'
Delete Task
Step 10: Explore CLI Tools
List Routes
Output:
โโโโโโโโโโณโโโโโโโโโโโโโโโโโโณโโโโโโโโโโโโโโโณโโโโโโโ
โ Method โ Path โ Handler โ Name โ
โกโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฉ
โ GET โ /tasks โ list_tasks โ - โ
โ GET โ /tasks/{id} โ get_task โ - โ
โ POST โ /tasks โ create_task โ - โ
โ PATCH โ /tasks/{id} โ update_task โ - โ
โ DELETE โ /tasks/{id} โ delete_task โ - โ
โโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโ
Check Application Health
What You've Learned
โ Models with Pydantic - Type-safe data structures โ Dependency Injection - Automatic service resolution โ Repository Pattern - Clean separation of concerns โ HTTP Routes - Decorator-based routing โ Configuration - Environment-based settings โ CLI Tools - Built-in development tools
Next Steps
Add a Frontend
Want to add a web UI? Initialize the frontend module:
This creates a styled homepage with Tailwind 4 and DaisyUI 5. Update your app.py to include the frontend module:
from myfy.core import Application
from myfy.web import WebModule
from myfy.frontend import FrontendModule, render_template # Add this
from config import AppSettings
from starlette.requests import Request # Add this
from starlette.templating import Jinja2Templates # Add this
# Create application
app = Application(settings_class=AppSettings, auto_discover=False)
app.add_module(WebModule())
app.add_module(FrontendModule()) # Add this line
# Add a frontend route
@route.get("/")
async def home(request: Request, templates: Jinja2Templates):
return render_template(
"home.html",
request=request,
templates=templates,
title="Task Management"
)
Now you have both an API (/tasks) and a web UI (/)!
The styled welcome page generated by myfy frontend init
Learn more: Frontend Module Documentation
Add Database Support
Replace the in-memory repository with SQLAlchemy:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
@provider(scope=SINGLETON)
async def database(settings: AppSettings):
engine = create_async_engine(settings.database_url)
return engine
@provider(scope=REQUEST)
async def session(db: Engine) -> AsyncSession:
async with AsyncSession(db) as session:
yield session
Add Authentication
@route.get("/tasks")
async def list_tasks(
repo: TaskRepository,
user: User = Depends(get_current_user)
) -> list[Task]:
return repo.list_by_user(user.id)
Add Testing
import pytest
from myfy.core import Application
@pytest.fixture
def app():
return Application(settings_class=TestSettings)
def test_create_task(app):
repo = app.container.get(TaskRepository)
task = repo.create("Test task")
assert task.id == 1
assert task.title == "Test task"
Full Project Structure
task-api/
โโโ app.py # Application entry point
โโโ config.py # Settings and providers
โโโ models.py # Pydantic models
โโโ repository.py # Data access layer
โโโ routes.py # HTTP handlers
โโโ .env # Environment variables
Compare with FastAPI
If you're coming from FastAPI, here's what's different:
| Feature | FastAPI | myfy |
|---|---|---|
| DI Registration | Inline Depends() |
@provider decorator |
| Scopes | No built-in scopes | SINGLETON, REQUEST, TASK |
| Config | Manual setup | BaseSettings with profiles |
| Modules | No concept | First-class modules |
| CLI | No built-in CLI | myfy run, myfy routes |
Troubleshooting
Module Not Found
# Make sure all files are in the same directory
ls -la
# Should see:
# app.py, config.py, models.py, repository.py, routes.py
Import Errors
# In app.py, import your modules explicitly
import config # This registers providers
import routes # This registers routes
Port in Use
Resources
- Quick Reference - Common patterns
- Core Concepts: DI - Deep dive
- Testing Guide - Test your application
- Deployment Guide - Go to production