name: layer-interfaces description: Use when creating or modifying code in nomarr/interfaces/. Covers API routes, CLI commands, and web handlers. Interfaces are thin adapters that validate inputs, call services, and serialize outputs.
Interfaces Layer
Purpose: Expose Nomarr to the outside world via HTTP (FastAPI), CLI (Typer), and web handlers.
Interfaces are thin adapters. They do three things:
- Validate inputs
- Call one service method
- Serialize outputs
No business logic lives here.
Allowed Imports
# ✅ Allowed
from nomarr.services import LibraryService, TaggingService
from nomarr.helpers.dto import LibraryDict, FileDict
from nomarr.interfaces.api.types import LibraryResponse # Pydantic models
Forbidden Imports
# ❌ NEVER import these in interfaces
from nomarr.workflows import ... # Call services, not workflows
from nomarr.components import ... # Call services, not components
from nomarr.persistence import ... # No direct DB access
The One Service Call Rule
Each route handler should call exactly one service method.
# ✅ Good - one service call
@router.get("/library/{library_id}")
def get_library(library_id: str, library_service: LibraryService = Depends(...)) -> LibraryResponse:
library = library_service.get_library(library_id)
return LibraryResponse.from_dto(library)
# ❌ Bad - multiple service calls
@router.post("/process/{library_id}")
def process(library_id: str, library_service: LibraryService = Depends(...), tagging_service: TaggingService = Depends(...)):
library = library_service.get_library(library_id)
tagging_service.tag_library(library.key) # ← Extract to service method
return {"status": "ok"}
If you need multiple service calls: Extract a service method that orchestrates them.
Data Flow
Request Flow
JSON → Pydantic Request Model → .to_dto() → Service (DTO)
Response Flow
Service (DTO) → .from_dto() → Pydantic Response Model → JSON
Pydantic models live only in interfaces. Never let them leak into services.
Authentication Rules
MANDATORY: All API endpoints require authentication except login.
Web API (/api/web/*)
- Uses
verify_sessionfor session token authentication - All routes MUST include
dependencies=[Depends(verify_session)]or_session: dict = Depends(verify_session)as a parameter - Exception:
/api/web/auth/loginis the only unauthenticated endpoint
v1 API (/api/v1/*)
- Uses
verify_keyfor API key authentication - All routes MUST include
dependencies=[Depends(verify_key)] - Exception:
/api/v1/public/*is intentionally public (version info)
API Consumer Separation — DO NOT MIX
| Router | Auth Method | Consumer | Frontend Calls? |
|---|---|---|---|
/api/web/* | Session token (verify_session) | Web frontend | YES |
/api/v1/* | API key (verify_key) | External tools (Navidrome, scripts) | NEVER |
The web frontend MUST ONLY call /api/web/* endpoints.
The v1 API uses API key authentication. The web frontend uses session authentication. These are incompatible — the frontend cannot authenticate to v1 endpoints.
If functionality exists in v1 but the frontend needs it, create a parallel route under web API.
Error Handling
- HTTP routes: Raise
HTTPException - CLI commands: Raise
typer.Exit(1) - Let services/workflows raise domain exceptions, catch them here
@router.get("/file/{file_key}")
def get_file(file_key: str, library_service: LibraryService = Depends(...)) -> FileResponse:
file = library_service.get_file(file_key)
if not file:
raise HTTPException(status_code=404, detail="File not found")
return FileResponse.from_dto(file)
Validation Checklist
Before committing interface code, verify:
- Does this file import from workflows, components, or persistence? → Violation
- Does this route call more than one service method? → Extract to service
- Does this route contain business logic (loops, branching, computation)? → Move to service
- Are Pydantic models staying in this layer only? → Services return DTOs
- Is the DTO-to-Pydantic conversion explicit? → Use
.from_dto() - Does this route have authentication? → Add
verify_session(web) orverify_key(v1) - Is the frontend calling
/api/v1/*? → Create web API route instead
Layer Scripts
This skill includes validation scripts in .github/skills/layer-interfaces/scripts/:
lint.py
Runs all linters on the interfaces layer:
python .github/skills/layer-interfaces/scripts/lint.py
Executes: ruff, mypy, vulture, bandit, radon, lint-imports
check_naming.py
Validates interfaces naming conventions:
python .github/skills/layer-interfaces/scripts/check_naming.py
Checks:
- Files must end in
_if.py - Route handlers should be thin (validate, call service, serialize)