---
paths: src/**/api/**/*.py
---

# API Layer Rules (MANDATORY)

The FastAPI API Layer handles only HTTP request/response processing. All business logic must be delegated to the Service Layer.

## ✅ Required Patterns

### 1. Service Injection via Depends()
```python
# ✅ CORRECT
@router.post("/companies", response_model=CompanyResponse)
async def create_company(
    request: CompanyCreate,
    service: CompanyService = Depends(get_company_service),  # DI
    user: FirebaseUserContext = Depends(verify_firebase_token),
) -> CompanyResponse:
    company = await service.create_company(request)
    return company
```

### 2. HTTP-Only Responsibilities
- Request validation (Pydantic)
- Response formatting
- HTTP status code setting
- Exception handling → HTTPException conversion

### 3. Query Parameter Validation
```python
# ✅ CORRECT - Query parameter validation
from fastapi import Query

@router.get("/search")
async def search_companies(
    q: str = Query(..., min_length=1, description="Search keyword"),
    limit: int = Query(10, ge=1, le=50),
    mode: str = Query("hybrid", pattern="^(vector|keyword|hybrid)$"),
):
    ...
```

### 4. BackgroundTasks Pattern
```python
# ✅ CORRECT - Background task handling
from fastapi import BackgroundTasks

@router.post("/companies", response_model=SuccessResponse[CompanyResponse])
async def create_company(
    request: CompanyCreate,
    background_tasks: BackgroundTasks,
    service: CompanyService = Depends(get_company_service),
) -> SuccessResponse[CompanyResponse]:
    # Pass background_tasks to Service
    company = await service.create_company(request, background_tasks)
    return SuccessResponse(message="Company created successfully", data=company)
```

### 5. SuccessResponse Wrapping (shared_libs)
```python
# ✅ CORRECT - Wrap response with SuccessResponse
from shared_libs.responses import SuccessResponse

@router.post("/companies", response_model=SuccessResponse[CompanyResponse])
async def create_company(
    request: CompanyCreate,
    service: CompanyService = Depends(get_company_service),
) -> SuccessResponse[CompanyResponse]:
    company = await service.create_company(request)
    return SuccessResponse(
        message="Company created successfully",
        data=company
    )

# ✅ CORRECT - GET endpoint
@router.get("/companies/{company_id}", response_model=SuccessResponse[CompanyResponse])
async def get_company(...) -> SuccessResponse[CompanyResponse]:
    company = await service.get_company(company_id)
    return SuccessResponse(message="Company retrieved successfully", data=company)
```

**Why SuccessResponse?**
- Consistent response format across microservices
- Compliance with shared_libs standard patterns
- Predictable response structure for clients

**SuccessResponse Structure:**
```json
{
  "success": true,
  "message": "Company created successfully",
  "data": { ... }
}
```

### 6. Optional Response Data
```python
# ✅ CORRECT - Nullable response
from typing import Optional

@router.get(
    "/{company_id}/hiring-info",
    response_model=SuccessResponse[Optional[HiringInfoResponse]]
)
async def get_hiring_info(
    company_id: UUID,
    service: CompanyService = Depends(get_company_service),
) -> SuccessResponse[Optional[HiringInfoResponse]]:
    data = await service.get_hiring_info(company_id)

    if data:
        return SuccessResponse(message="Hiring info found", data=data)

    return SuccessResponse(message="No hiring info available", data=None)
```

### 7. HTTP Status Codes
```python
# ✅ CORRECT - Explicit status code usage
from fastapi import status

@router.post("", status_code=status.HTTP_201_CREATED)  # Resource creation
async def create_company(...):
    ...

@router.post("/{id}/grounding", status_code=status.HTTP_202_ACCEPTED)  # Async operation
async def trigger_grounding(...):
    ...

@router.delete("/{id}", status_code=status.HTTP_200_OK)  # Delete with response body
async def delete_company(...) -> SuccessResponse[None]:
    ...
```

---

## ❌ Forbidden Patterns

### 1. No Direct Repository Calls
```python
# ❌ WRONG - Direct Repository import and usage
from company_service.repositories import CompanyRepository

@router.post("/jobs/refresh-stale")
async def refresh_stale_companies(
    repository: CompanyRepository = Depends(get_company_repository),  # ❌ Direct Repository injection
):
    stale = await repository.get_stale_companies(...)  # ❌ Direct Repository call

# ❌ WRONG - Accessing service.repository
@router.get("/search")
async def search_companies(
    service: CompanyService = Depends(get_company_service),
):
    # ❌ Direct access to Service's internal repository
    companies = await service.repository.vector_search(...)

# ✅ CORRECT - Use Service methods
@router.post("/jobs/refresh-stale")
async def refresh_stale_companies(
    service: CompanyService = Depends(get_company_service),  # ✅ Service injection
):
    stale = await service.get_stale_companies(...)  # ✅ Service method call

@router.get("/search")
async def search_companies(
    service: CompanyService = Depends(get_company_service),
):
    # ✅ Encapsulated via Service method
    companies = await service.vector_search(...)
```

### 2. No Manual Service Instantiation
```python
# ❌ WRONG - Manual Service creation
@router.get("/companies")
async def get_companies():
    service = CompanyService()  # ❌ NEVER DO THIS
    return await service.get_all()
```

### 3. No Business Logic in API
```python
# ❌ WRONG - Business logic in API
@router.post("/companies")
async def create_company(request: CompanyCreate):
    # ❌ This logic should be in Service
    slug = request.name.lower().replace(" ", "-")
    if await check_duplicate(slug):
        raise HTTPException(400, "Duplicate company")

    # ❌ Score calculation should be in Service
    if request.employee_count > 1000:
        score = 90
    else:
        score = 50
```

### 4. No Direct Response Without SuccessResponse
```python
# ❌ WRONG - Direct Pydantic model return
@router.get("/companies/{company_id}", response_model=CompanyResponse)
async def get_company(...) -> CompanyResponse:
    return await service.get_company(company_id)  # ❌ NO SuccessResponse wrapping

# ✅ CORRECT - Wrap with SuccessResponse
@router.get("/companies/{company_id}", response_model=SuccessResponse[CompanyResponse])
async def get_company(...) -> SuccessResponse[CompanyResponse]:
    company = await service.get_company(company_id)
    return SuccessResponse(message="Company retrieved successfully", data=company)
```

---

## ✅ Exception Handling Pattern

```python
# ✅ CORRECT - Convert Service exception to HTTP exception
from fastapi import HTTPException

@router.get("/companies/{company_id}", response_model=SuccessResponse[CompanyResponse])
async def get_company(
    company_id: UUID,
    service: CompanyService = Depends(get_company_service),
) -> SuccessResponse[CompanyResponse]:
    company = await service.get_company(company_id)
    if not company:
        raise HTTPException(status_code=404, detail="Company not found")
    return SuccessResponse(message="Company retrieved successfully", data=company)
```

---

## ✅ Path Parameter Validation

```python
# ✅ CORRECT - Path parameter validation
from fastapi import Path

@router.get("/companies/{company_id}")
async def get_company(
    company_id: UUID = Path(..., description="Company UUID"),
    service: CompanyService = Depends(get_company_service),
) -> SuccessResponse[CompanyResponse]:
    ...

@router.get("/companies/{slug}")
async def get_company_by_slug(
    slug: str = Path(..., min_length=1, max_length=100, pattern="^[a-z0-9-]+$"),
    service: CompanyService = Depends(get_company_service),
) -> SuccessResponse[CompanyResponse]:
    ...
```

---

## ✅ Multiple Service Injection

```python
# ✅ CORRECT - Multiple Service injection
@router.post("/companies/{company_id}/grounding", response_model=SuccessResponse[CompanyResponse])
async def trigger_grounding(
    company_id: UUID,
    company_service: CompanyService = Depends(get_company_service),
    grounding_service: GroundingService = Depends(get_grounding_service),
) -> SuccessResponse[CompanyResponse]:
    company = await company_service.get_company(company_id)
    if not company:
        raise HTTPException(status_code=404, detail="Company not found")

    await grounding_service.process_grounding(company_id)
    updated = await company_service.get_company(company_id)
    return SuccessResponse(message="Grounding completed", data=updated)
```

---

## ✅ Pagination Response Pattern

```python
# ✅ CORRECT - Pagination response
from shared_libs.responses import SuccessResponse, PaginatedResponse

@router.get("/companies", response_model=SuccessResponse[PaginatedResponse[CompanyResponse]])
async def list_companies(
    page: int = Query(1, ge=1, description="Page number"),
    size: int = Query(20, ge=1, le=100, description="Items per page"),
    service: CompanyService = Depends(get_company_service),
) -> SuccessResponse[PaginatedResponse[CompanyResponse]]:
    result = await service.list_companies(page=page, size=size)
    return SuccessResponse(
        message="Companies retrieved successfully",
        data=PaginatedResponse(
            items=result.items,
            total=result.total,
            page=page,
            size=size,
            pages=result.pages,
        )
    )
```

---

## 📋 Checklist

**Service Layer Communication:**
- [ ] Are all Services injected via `Depends()`?
- [ ] No direct Repository imports?
- [ ] **No direct `service.repository` access?** (Critical!)
- [ ] Only calling Service methods?

**Response Format:**
- [ ] **Are all responses wrapped with `SuccessResponse`?** (shared_libs standard)
- [ ] Using `response_model=SuccessResponse[DataType]` format?
- [ ] Optional data expressed as `SuccessResponse[Optional[T]]`?

**HTTP Handling:**
- [ ] No business logic included?
- [ ] Only performing HTTP-related operations?
- [ ] Query parameter validation using `Query()`?
- [ ] Appropriate HTTP status codes specified? (201, 202, 200, etc.)
- [ ] Background tasks using `BackgroundTasks`?
