Skip to content

Reference

Complete single-page reference for myfy framework. Use Ctrl+F to find what you need.


Quick Start

Minimal Application

from myfy.core import Application
from myfy.web import route, WebModule

@route.get("/")
async def home() -> dict:
    return {"message": "Hello World"}

app = Application(auto_discover=False)
app.add_module(WebModule())
uv run myfy run

Full-Stack with Frontend

uv run myfy frontend init
uv run myfy run

Installation Guide · Tutorial


Dependency Injection

Provider Scopes

Scope Lifetime Use Case
SINGLETON Application Database pools, caches, services
REQUEST HTTP Request Database sessions, request context
TASK Background Task Task-specific resources

Register Providers

from myfy.core import provider, SINGLETON, REQUEST

@provider(scope=SINGLETON)
def database(settings: Settings) -> Database:
    return Database(settings.db_url)

@provider(scope=REQUEST)
def session(db: Database) -> Session:
    return db.create_session()

Inject in Routes

@route.get("/users")
async def list_users(db: Database, cache: Cache) -> list[User]:
    return await db.get_all_users()

DI Deep Dive


Web Routes

HTTP Methods

from myfy.web import route

@route.get("/users")           # GET
@route.post("/users")          # POST
@route.put("/users/{id}")      # PUT
@route.patch("/users/{id}")    # PATCH
@route.delete("/users/{id}")   # DELETE

Path Parameters

@route.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
    # user_id automatically converted to int
    return await service.get_user(user_id)

@route.get("/posts/{post_id}/comments/{comment_id}")
async def get_comment(post_id: int, comment_id: int) -> Comment:
    return await service.get_comment(post_id, comment_id)

Query Parameters

Basic query parameters with defaults:

@route.get("/users")
async def list_users(
    page: int = 1,
    limit: int = 10,
    sort: str = "name"
) -> list[User]:
    return await service.list_users(page, limit, sort)

# GET /users?page=2&limit=20&sort=created_at

Query parameters with validation (new in v0.1.2):

from myfy.web import route, Query

@route.get("/search")
async def search(
    q: str = Query(min_length=1, max_length=100),
    limit: int = Query(default=20, ge=1, le=100),
    page: int = Query(default=1, ge=1),
) -> list[Result]:
    return await service.search(q, limit, page)

Query constraints:

Constraint Type Description
ge int/float Greater than or equal
le int/float Less than or equal
gt int/float Greater than
lt int/float Less than
min_length int Minimum string length
max_length int Maximum string length
pattern str Regex pattern
alias str Alternative query param name

Request Body

from pydantic import BaseModel, Field

class CreateUserDTO(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
    age: int = Field(ge=0, le=150)

@route.post("/users")
async def create_user(body: CreateUserDTO) -> User:
    return await service.create_user(body)

Status Codes

@route.post("/users", status_code=201)
async def create_user(body: CreateUserDTO) -> User:
    return await service.create_user(body)

@route.delete("/users/{id}", status_code=204)
async def delete_user(id: int) -> None:
    await service.delete_user(id)

Response Types

# Dict → JSON
@route.get("/user")
async def get_user() -> dict:
    return {"id": 1, "name": "John"}

# Pydantic model → JSON
@route.get("/user")
async def get_user() -> User:
    return User(id=1, name="John")

# Custom response
from starlette.responses import RedirectResponse

@route.get("/redirect")
async def redirect():
    return RedirectResponse(url="/home")

Web Module


Error Handling

WebError Hierarchy (new in v0.1.2)

All errors follow RFC 7807 Problem Details format:

from myfy.web import errors

# Built-in errors
raise errors.NotFound("User not found")
raise errors.BadRequest("Invalid email format", field="email")
raise errors.Unauthorized("Invalid token")
raise errors.Forbidden("Access denied")
raise errors.Conflict("Username already taken")
raise errors.RateLimit("Too many requests", retry_after=60)

Available errors:

Error Status Use Case
errors.BadRequest 400 Invalid input, validation failures
errors.Unauthorized 401 Missing or invalid authentication
errors.Forbidden 403 Authenticated but not authorized
errors.NotFound 404 Resource doesn't exist
errors.Conflict 409 Duplicate entries, state conflicts
errors.UnprocessableEntity 422 Valid syntax, invalid semantics
errors.RateLimit 429 Too many requests
errors.ServiceUnavailable 503 Temporary outage

Custom Errors

from myfy.web.exceptions import WebError

class PaymentRequiredError(WebError):
    status_code = 402
    error_type = "payment_required"

raise PaymentRequiredError("Insufficient credits", required=100)

Response Format

Errors automatically return RFC 7807 Problem Details:

{
    "type": "not_found",
    "title": "NotFoundError",
    "status": 404,
    "detail": "User not found",
    "user_id": 123
}

Configuration

Settings Class

from myfy.core import BaseSettings
from pydantic import Field

class Settings(BaseSettings):
    app_name: str = Field(default="My App")
    debug: bool = Field(default=False)
    database_url: str
    api_key: str = Field(alias="API_KEY")

Environment Files

# .env (default)
APP_NAME=My App
DATABASE_URL=postgresql://localhost/mydb

# .env.dev (MYFY_PROFILE=dev)
DEBUG=true
DATABASE_URL=postgresql://localhost/mydb_dev

# .env.prod (MYFY_PROFILE=prod)
DEBUG=false
DATABASE_URL=postgresql://prod-server/mydb

Load Profile

MYFY_PROFILE=dev uv run myfy run
MYFY_PROFILE=prod uv run myfy run --no-reload

Configuration Guide


Modules

Create Module

from myfy.core import BaseModule, Container

class DataModule(BaseModule):
    def __init__(self):
        super().__init__("data")

    def configure(self, container: Container) -> None:
        container.register(Database, factory=create_db, scope=SINGLETON)

    async def start(self) -> None:
        self.db = await connect_database()

    async def stop(self) -> None:
        await self.db.disconnect()

Register Module

app = Application(auto_discover=False)
app.add_module(DataModule())
app.add_module(WebModule())

Built-in Modules

Module Package Purpose
WebModule myfy-web HTTP/ASGI routing
DataModule myfy-data Async SQLAlchemy
FrontendModule myfy-frontend Tailwind, DaisyUI, Vite

Modules Guide


Database (myfy-data)

Setup

from myfy.core import Application
from myfy.data import DataModule, Base
from myfy.web import route, WebModule
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.asyncio import AsyncSession

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String(100))

app = Application(auto_discover=False)
app.add_module(DataModule(
    auto_create_tables=True,  # Dev only
    metadata=Base.metadata,
))
app.add_module(WebModule())

CRUD Operations

from sqlalchemy import select

# Create
@route.post("/users", status_code=201)
async def create_user(body: CreateUserDTO, session: AsyncSession) -> dict:
    user = User(name=body.name, email=body.email)
    session.add(user)
    await session.commit()
    await session.refresh(user)
    return {"id": user.id, "name": user.name}

# Read
@route.get("/users/{user_id}")
async def get_user(user_id: int, session: AsyncSession) -> dict:
    result = await session.execute(
        select(User).where(User.id == user_id)
    )
    user = result.scalar_one_or_none()
    if not user:
        raise errors.NotFound("User not found")
    return {"id": user.id, "name": user.name}

# Update
@route.put("/users/{user_id}")
async def update_user(user_id: int, body: UpdateUserDTO, session: AsyncSession) -> dict:
    result = await session.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if not user:
        raise errors.NotFound("User not found")
    user.name = body.name
    await session.commit()
    return {"id": user.id, "name": user.name}

# Delete
@route.delete("/users/{user_id}", status_code=204)
async def delete_user(user_id: int, session: AsyncSession) -> None:
    result = await session.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if not user:
        raise errors.NotFound("User not found")
    await session.delete(user)
    await session.commit()

Database Configuration

# .env
MYFY_DATA_DATABASE_URL=postgresql+asyncpg://user:pass@localhost/db
MYFY_DATA_POOL_SIZE=10
MYFY_DATA_MAX_OVERFLOW=20

Supported databases:

Database URL Format
SQLite sqlite+aiosqlite:///./myfy.db
PostgreSQL postgresql+asyncpg://user:pass@host/db
MySQL mysql+aiomysql://user:pass@host/db

Data Module


Frontend (myfy-frontend)

Initialize

uv run myfy frontend init

Creates: - frontend/templates/ - Jinja2 templates - frontend/css/ - Tailwind CSS - frontend/js/ - JavaScript - package.json - Node dependencies (Vite, Tailwind 4, DaisyUI 5)

Render Templates

from myfy.frontend import render_template

@route.get("/")
async def home(request: Request, templates: Jinja2Templates):
    return render_template(
        "home.html",
        request=request,
        templates=templates,
        title="Welcome"
    )

Template Structure

{# templates/home.html #}
{% extends "base.html" %}

{% block content %}
<div class="hero min-h-screen bg-base-200">
  <div class="hero-content text-center">
    <h1 class="text-5xl font-bold">{{ title }}</h1>
    <button class="btn btn-primary">Get Started</button>
  </div>
</div>
{% endblock %}

DaisyUI Components

<!-- Buttons -->
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>

<!-- Card -->
<div class="card bg-base-100 shadow-xl">
  <div class="card-body">
    <h2 class="card-title">Card Title</h2>
    <p>Card content</p>
  </div>
</div>

<!-- Form -->
<input type="text" class="input input-bordered" />
<button class="btn btn-primary">Submit</button>

Build for Production

uv run myfy frontend build

Frontend Module


CLI Commands

Command Description
myfy run Start development server
myfy routes List all routes
myfy modules Show loaded modules
myfy doctor Validate configuration
myfy frontend init Initialize frontend
myfy frontend build Build for production

Run Options

uv run myfy run                    # Default (localhost:8000)
uv run myfy run --port 8080        # Custom port
uv run myfy run --host 0.0.0.0     # Bind all interfaces
uv run myfy run --no-reload        # Disable auto-reload

CLI Reference


Middleware

from myfy.web import WebModule
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware

app.add_module(WebModule(
    middleware=[
        Middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_methods=["*"],
            allow_headers=["*"]
        )
    ]
))

Testing

Basic Test

import pytest
from myfy.core import Application

@pytest.fixture
def app():
    app = Application(settings_class=TestSettings, auto_discover=False)
    app.add_module(WebModule())
    app.initialize()
    return app

def test_repository(app):
    repo = app.container.get(TaskRepository)
    task = repo.create("Test task")
    assert task.id == 1

HTTP Tests

from starlette.testclient import TestClient
from myfy.web import WebModule

def test_get_users(app):
    web_module = app.get_module(WebModule)
    client = TestClient(web_module.get_asgi_app(app.container))
    response = client.get("/users")
    assert response.status_code == 200

Common Patterns

Repository Pattern

class UserRepository:
    def __init__(self, session: AsyncSession):
        self.session = session

    async def get(self, user_id: int) -> User | None:
        result = await self.session.execute(
            select(User).where(User.id == user_id)
        )
        return result.scalar_one_or_none()

    async def create(self, user: User) -> User:
        self.session.add(user)
        await self.session.commit()
        return user

Service Layer

class UserService:
    def __init__(self, settings: Settings):
        self.settings = settings

    async def get_user(self, user_id: int, session: AsyncSession) -> User:
        repo = UserRepository(session)
        user = await repo.get(user_id)
        if not user:
            raise errors.NotFound(f"User {user_id} not found")
        return user

@provider(scope=SINGLETON)
def user_service(settings: Settings) -> UserService:
    return UserService(settings)

Background Tasks

from starlette.background import BackgroundTask
from starlette.responses import Response

async def send_email_task(to: str, subject: str):
    pass  # Send email

@route.post("/send-email")
async def send_email(body: EmailDTO) -> Response:
    return Response(
        content='{"status": "queued"}',
        background=BackgroundTask(send_email_task, body.to, body.subject)
    )

Application Lifecycle

Init Phase    →  Load config, create container
Start Phase   →  Start modules in order
Run Phase     →  Execute application
Stop Phase    →  Stop modules in reverse order

Lifecycle Guide


Project Structure

my-project/
├── app.py                    # Application entry
├── .env                      # Environment variables
├── pyproject.toml            # Dependencies
├── services/
│   ├── __init__.py
│   └── user_service.py       # Business logic
├── models/
│   ├── __init__.py
│   └── user.py               # SQLAlchemy models
├── routes/
│   ├── __init__.py
│   └── users.py              # Route handlers
└── frontend/                 # (if using myfy-frontend)
    ├── templates/
    ├── css/
    └── js/

Package Installation

# Full framework (all modules)
pip install myfy

# Individual packages
pip install myfy-core       # Core DI, config, modules
pip install myfy-web        # HTTP/ASGI routing
pip install myfy-data       # Async SQLAlchemy
pip install myfy-frontend   # Tailwind, DaisyUI, Vite
pip install myfy-cli        # Development commands

Deep Dives

Topic Link
Dependency Injection Core Concepts: DI
Module System Core Concepts: Modules
Configuration Core Concepts: Configuration
Application Lifecycle Core Concepts: Lifecycle
Web Module Modules: Web
Data Module Modules: Data
Frontend Module Modules: Frontend
CLI Tools Modules: CLI
Full Tutorial Getting Started: Tutorial