---
name: refactor-endpoint
description: Refactor a FastAPI endpoint from monolithic handler to service layer pattern — extract business logic, add error handling, fix DI
---

# Refactor Endpoint to Service Layer

Break a monolithic endpoint handler into the layered pattern: thin endpoint -> service -> repository.

## When to Use

When an endpoint handler:
- Exceeds 30 lines
- Directly calls BigQuery or Sayari API
- Contains business logic (data transformation, scoring, filtering)
- Has no error handling around external calls
- Uses global state instead of dependency injection

## Process

### Step 1: Identify Responsibilities

Read the endpoint and list every distinct operation:
1. Input validation (should stay in endpoint via Pydantic)
2. BigQuery queries (-> repository)
3. Sayari API calls (-> client service)
4. Data transformation (-> service)
5. Response formatting (-> service or endpoint)

### Step 2: Create Repository (if BigQuery involved)

`backend/app/repositories/bigquery_repo.py`:
```python
"""BigQuery data access layer."""
from google.cloud import bigquery
from app.core.query_manager import QueryManager

class BigQueryRepository:
    def __init__(self, client: bigquery.Client, query_manager: QueryManager):
        self.client = client
        self.qm = query_manager

    def run_query(self, template: str, params: dict) -> list[dict]:
        """Execute a query with error handling."""
        try:
            sql = self.qm.render(template, params)
            job_config = self.qm.create_job_config(**params)
            result = self.client.query(sql, job_config=job_config)
            return [dict(row) for row in result]
        except Exception as e:
            raise BigQueryError(f"Query failed: {e}") from e
```

### Step 3: Create Service

`backend/app/services/<domain>_service.py`:
```python
"""Business logic for <domain>."""

class DomainService:
    def __init__(self, repo: BigQueryRepository, sayari_client: SayariClient):
        self.repo = repo
        self.sayari = sayari_client

    def process(self, params, user_context) -> Result:
        """Orchestrate: query -> enrich -> transform."""
        raw_data = self.repo.run_query(params)
        enriched = self.sayari.enrich_entities(raw_data)
        return self._transform(enriched)
```

### Step 4: Slim Down Endpoint

```python
@router.get("/resource/")
async def get_resource(
    params: ResourceParams = Depends(),
    service: DomainService = Depends(get_domain_service),
    user: UserContext = Depends(get_user_context),
):
    """Thin HTTP handler — delegates to service."""
    try:
        result = service.process(params, user)
        return result
    except BigQueryError as e:
        raise HTTPException(status_code=503, detail=str(e))
    except SayariAPIError as e:
        raise HTTPException(status_code=502, detail=str(e))
```

### Step 5: Update Tests

Update tests to:
- Test the service directly (unit tests, fast)
- Test the endpoint via TestClient (integration tests, with DI overrides)

### Step 6: Verify

```bash
cd backend && python -m pytest tests/ -v
cd backend && python -c "from app.main import app; print('Import OK')"
```

## Checklist
- [ ] Endpoint handler <= 30 lines
- [ ] All external calls in repository or client (not in endpoint)
- [ ] All external calls wrapped in try/except
- [ ] Business logic in service (testable without HTTP)
- [ ] Dependencies injected via `Depends()`
- [ ] No global mutable state accessed directly
- [ ] Tests updated for new structure
