home / skills / psincraian / myfy / routing-api

This skill helps you design and validate web routes with FastAPI-like decorators, supporting path, query, body parameters, auth, rate limits, and errors.

npx playbooks add skill psincraian/myfy --skill routing-api

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.6 KB
---
name: routing-api
description: myfy web routing with FastAPI-like decorators. Use when working with WebModule, @route decorators, path parameters, query parameters, request bodies, AuthModule for authentication, RateLimitModule for rate limiting, or error handling.
---

# Web Routing in myfy

myfy provides FastAPI-like routing with full DI integration.

## Route Decorators

```python
from myfy.web import route

@route.get("/path")
async def handler() -> dict:
    return {"message": "hello"}

@route.post("/path", status_code=201)
async def create() -> dict:
    return {"created": True}

@route.put("/path/{id}")
async def update(id: int) -> dict:
    return {"updated": id}

@route.delete("/path/{id}", status_code=204)
async def delete(id: int) -> None:
    pass

@route.patch("/path/{id}")
async def partial_update(id: int) -> dict:
    return {"patched": id}
```

## Path Parameters

Extract from URL template using `{param}`:

```python
@route.get("/users/{user_id}/posts/{post_id}")
async def get_post(user_id: int, post_id: int) -> dict:
    return {"user": user_id, "post": post_id}
```

Path parameters are:
- Automatically type-converted based on annotation
- Must match function parameter names exactly
- Must be valid Python identifiers

## Query Parameters

Use `Query` for explicit query parameters:

```python
from myfy.web import Query

@route.get("/search")
async def search(
    q: str = Query(default=""),           # With default value
    limit: int = Query(default=10),       # Integer query param
    page: int = Query(alias="p"),         # Aliased (?p=1 in URL)
) -> dict:
    return {"query": q, "limit": limit, "page": page}
```

## Request Body

Use Pydantic models or dataclasses for request bodies:

```python
from pydantic import BaseModel

class UserCreate(BaseModel):
    email: str
    name: str

@route.post("/users", status_code=201)
async def create_user(body: UserCreate, session: AsyncSession) -> dict:
    user = User(**body.model_dump())
    session.add(user)
    await session.commit()
    return {"id": user.id}
```

Request bodies are automatically:
- Parsed from JSON
- Validated by Pydantic
- Type-checked at runtime

## Parameter Classification

Parameters are classified in this order:

1. **Path parameters** - Names matching `{param}` in route path
2. **Query parameters** - Annotated with `Query(...)`
3. **Body parameter** - Pydantic model, dataclass, or dict
4. **DI dependencies** - Everything else (resolved from container)

```python
@route.post("/users/{user_id}/orders")
async def create_order(
    user_id: int,                    # 1. Path param (matches {user_id})
    limit: int = Query(default=10),  # 2. Query param (explicit Query)
    body: OrderCreate,               # 3. Request body (Pydantic model)
    session: AsyncSession,           # 4. DI dependency
    settings: AppSettings,           # 4. DI dependency
) -> dict:
    ...
```

## Authentication

Use `Authenticated` for protected routes:

```python
from myfy.web import Authenticated, AuthModule
from dataclasses import dataclass

@dataclass
class User(Authenticated):
    email: str

# Register auth provider
def my_auth(request: Request) -> User | None:
    token = request.headers.get("Authorization")
    if not token:
        return None  # Results in 401
    return User(id="123", email="[email protected]")

app.add_module(AuthModule(authenticated_provider=my_auth))

# Protected route - returns 401 if not authenticated
@route.get("/profile")
async def profile(user: User) -> dict:
    return {"id": user.id, "email": user.email}
```

## Error Handling

### Quick Errors with abort()

```python
from myfy.web import abort

@route.get("/users/{user_id}")
async def get_user(user_id: int, session: AsyncSession) -> dict:
    user = await session.get(User, user_id)
    if not user:
        abort(404, "User not found")
    return {"user": user}
```

### Typed Errors

```python
from myfy.web import errors

raise errors.NotFound("User not found")
raise errors.BadRequest("Invalid email", field="email")
raise errors.Unauthorized("Invalid token")
raise errors.Forbidden("Access denied")
raise errors.Conflict("Email already exists")
```

### Custom Exceptions

```python
from myfy.web.exceptions import WebError

class RateLimitExceeded(WebError):
    status_code = 429
    error_type = "rate_limit_exceeded"
```

## Rate Limiting

```python
from myfy.web.ratelimit import RateLimitModule, rate_limit, RateLimitKey

# Add module
app.add_module(RateLimitModule())

# Rate limit by IP (default)
@route.get("/api/data")
@rate_limit(100)  # 100 requests per minute per IP
async def get_data() -> dict:
    ...

# Rate limit by authenticated user
@route.get("/api/profile")
@rate_limit(50, key=RateLimitKey.USER)
async def get_profile(user: User) -> dict:
    ...
```

## Response Types

Routes can return:

```python
# Dict (serialized to JSON)
@route.get("/json")
async def json_response() -> dict:
    return {"key": "value"}

# Pydantic model (serialized to JSON)
@route.get("/model")
async def model_response() -> UserResponse:
    return UserResponse(id=1, name="John")

# None for 204 No Content
@route.delete("/users/{id}", status_code=204)
async def delete_user(id: int) -> None:
    ...
```

## Best Practices

1. **Always use async** - All handlers should be async functions
2. **Type all parameters** - Use type hints for auto-classification
3. **Use Pydantic for bodies** - Get free validation
4. **Return typed responses** - Prefer Pydantic models over dicts
5. **Use appropriate status codes** - 201 for creation, 204 for deletion
6. **Handle errors explicitly** - Use abort() or typed errors
7. **Document with docstrings** - Add OpenAPI-compatible docs

Overview

This skill implements FastAPI-like web routing for myfy, providing decorator-based route registration, automatic parameter classification, request body parsing, authentication hooks, error helpers, and rate limiting. It integrates with dependency injection so handlers receive database sessions, settings, and other services directly. Use it to build HTTP endpoints with typed inputs, Pydantic validation, and predictable response handling.

How this skill works

Define routes with @route.<method>(path) decorators; myfy inspects the function signature to classify parameters as path, query, body, or DI dependencies. Request bodies are parsed and validated with Pydantic or dataclasses. Authentication and rate limiting are provided by optional modules (AuthModule, RateLimitModule), and errors can be raised with abort() or typed WebError subclasses.

When to use it

  • Creating REST endpoints with typed path, query, and body parameters
  • Injecting DB sessions, settings, or other services via dependency injection
  • Protecting routes with authentication providers via AuthModule
  • Applying per-route rate limits with RateLimitModule and decorators
  • Returning Pydantic models or dicts for automatic JSON serialization

Best practices

  • Make all handlers async to avoid blocking the event loop
  • Type all parameters to enable automatic classification and conversion
  • Use Pydantic models for request bodies to get validation and clear docs
  • Return typed responses (Pydantic models preferred) and use proper status codes
  • Use abort() or typed errors for predictable HTTP error responses
  • Register AuthModule and RateLimitModule only when needed to keep middleware minimal

Example use cases

  • CRUD endpoints: path parameters for resource ids, Pydantic models for create/update bodies, and 201/204 status codes
  • Authenticated profile route that returns the current user or 401 when unauthenticated
  • Search endpoints using Query for pagination and aliased query params
  • Per-user rate-limited API endpoints using rate_limit(..., key=RateLimitKey.USER)
  • Services exposing typed responses (Pydantic models) for automatic OpenAPI generation

FAQ

How are parameters classified automatically?

Parameters are classified in order: path parameters (matching {name} in route), explicit Query(...) parameters, a single body parameter (Pydantic/dataclass/dict), and remaining parameters are resolved from the DI container.

How do I protect a route with authentication?

Register AuthModule with an authenticated_provider that returns a User or None. Add a parameter typed as your Authenticated dataclass; requests without valid credentials return 401 automatically.