# SkillPatch skill: fastapi-router-py

This skill guides agents in creating FastAPI routers with full CRUD operations, authentication dependencies, and proper response models. It provides copy-paste templates, naming conventions, authentication patterns (optional and required), HTTP status code usage, and integration steps for wiring routers into a FastAPI application. Best practices for sync vs. async handlers and resource lifecycle management are also included.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/fastapi-router-py
curl -sSL https://skillpatch.dev/install_skill/fastapi-router-py | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (2)

- `SKILL.md`
- `assets/template.py`


### `SKILL.md`

````markdown
---
name: fastapi-router-py
description: Create FastAPI routers with CRUD operations, authentication dependencies, and proper response models. Use when building REST API endpoints, creating new routes, implementing CRUD operations, or adding authenticated endpoints in FastAPI applications.
license: MIT
metadata:
  author: Microsoft
  version: "1.0.0"
---

# FastAPI Router

Create FastAPI routers following established patterns with proper authentication, response models, and HTTP status codes.

## Quick Start

Copy the template from [assets/template.py](assets/template.py) and replace placeholders:
- `{{ResourceName}}` → PascalCase name (e.g., `Project`)
- `{{resource_name}}` → snake_case name (e.g., `project`)
- `{{resource_plural}}` → plural form (e.g., `projects`)

## Authentication Patterns

```python
# Optional auth - returns None if not authenticated
current_user: Optional[User] = Depends(get_current_user)

# Required auth - raises 401 if not authenticated
current_user: User = Depends(get_current_user_required)
```

## Response Models

```python
@router.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: str) -> Item:
    ...

@router.get("/items", response_model=list[Item])
async def list_items() -> list[Item]:
    ...
```

## HTTP Status Codes

```python
@router.post("/items", status_code=status.HTTP_201_CREATED)
@router.delete("/items/{id}", status_code=status.HTTP_204_NO_CONTENT)
```

## Integration Steps

1. Create router in `src/backend/app/routers/`
2. Mount in `src/backend/app/main.py`
3. Create corresponding Pydantic models
4. Create service layer if needed
5. Add frontend API functions

## Best Practices

1. **Pick `def` or `async def` per endpoint based on whether you call async I/O;** do not mix sync and async blocking calls in one handler.
2. **Manage long-lived resources (DB pools, HTTP clients) in `lifespan` and inject via `Depends`;** use `with`/`async with` for per-request resources.

````


### `assets/template.py`

```
"""
{{ResourceName}} Router

Handles CRUD operations for {{resource_name}} resources.
"""

from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status

from app.auth.jwt import get_current_user, get_current_user_required
from app.models.user import User
from app.models.{{resource_name}} import (
    {{ResourceName}},
    {{ResourceName}}Create,
    {{ResourceName}}Update,
)
from app.services.{{resource_name}}_service import {{ResourceName}}Service

router = APIRouter(prefix="/api", tags=["{{resource_plural}}"])


# ============================================================================
# Dependencies
# ============================================================================


def get_service() -> {{ResourceName}}Service:
    """Dependency to get service instance."""
    return {{ResourceName}}Service()


# ============================================================================
# Endpoints
# ============================================================================


@router.get("/{{resource_plural}}", response_model=list[{{ResourceName}}])
async def list_{{resource_plural}}(
    limit: int = Query(default=50, ge=1, le=100),
    offset: int = Query(default=0, ge=0),
    current_user: Optional[User] = Depends(get_current_user),
    service: {{ResourceName}}Service = Depends(get_service),
) -> list[{{ResourceName}}]:
    """
    List all {{resource_plural}}.

    - **limit**: Maximum number of items to return (1-100)
    - **offset**: Number of items to skip
    """
    return await service.list_{{resource_plural}}(limit=limit, offset=offset)


@router.get("/{{resource_plural}}/{{{resource_name}}_id}", response_model={{ResourceName}})
async def get_{{resource_name}}(
    {{resource_name}}_id: str,
    current_user: Optional[User] = Depends(get_current_user),
    service: {{ResourceName}}Service = Depends(get_service),
) -> {{ResourceName}}:
    """
    Get a specific {{resource_name}} by ID.

    Raises 404 if not found.
    """
    result = await service.get_{{resource_name}}_by_id({{resource_name}}_id)
    if result is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="{{ResourceName}} not found",
        )
    return result


@router.post(
    "/{{resource_plural}}",
    response_model={{ResourceName}},
    status_code=status.HTTP_201_CREATED,
)
async def create_{{resource_name}}(
    data: {{ResourceName}}Create,
    current_user: User = Depends(get_current_user_required),
    service: {{ResourceName}}Service = Depends(get_service),
) -> {{ResourceName}}:
    """
    Create a new {{resource_name}}.

    Requires authentication.
    """
    return await service.create_{{resource_name}}(data, current_user.id)


@router.patch("/{{resource_plural}}/{{{resource_name}}_id}", response_model={{ResourceName}})
async def update_{{resource_name}}(
    {{resource_name}}_id: str,
    data: {{ResourceName}}Update,
    current_user: User = Depends(get_current_user_required),
    service: {{ResourceName}}Service = Depends(get_service),
) -> {{ResourceName}}:
    """
    Update an existing {{resource_name}}.

    Requires authentication and ownership.
    """
    # Verify ownership
    existing = await service.get_{{resource_name}}_by_id({{resource_name}}_id)
    if existing is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="{{ResourceName}} not found",
        )

    # Optional: Check ownership
    # if existing.author_id != current_user.id:
    #     raise HTTPException(
    #         status_code=status.HTTP_403_FORBIDDEN,
    #         detail="Not authorized to update this {{resource_name}}",
    #     )

    return await service.update_{{resource_name}}({{resource_name}}_id, data)


@router.delete(
    "/{{resource_plural}}/{{{resource_name}}_id}",
    status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_{{resource_name}}(
    {{resource_name}}_id: str,
    current_user: User = Depends(get_current_user_required),
    service: {{ResourceName}}Service = Depends(get_service),
) -> None:
    """
    Delete a {{resource_name}}.

    Requires authentication and ownership.
    """
    existing = await service.get_{{resource_name}}_by_id({{resource_name}}_id)
    if existing is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="{{ResourceName}} not found",
        )

    await service.delete_{{resource_name}}({{resource_name}}_id)

```
