Claude-skill-registry layer-interfaces

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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/layer-interfaces" ~/.claude/skills/majiayu000-claude-skill-registry-layer-interfaces && rm -rf "$T"
manifest: skills/data/layer-interfaces/SKILL.md
source content

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:

  1. Validate inputs
  2. Call one service method
  3. 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_session
    for session token authentication
  • All routes MUST include
    dependencies=[Depends(verify_session)]
    or
    _session: dict = Depends(verify_session)
    as a parameter
  • Exception:
    /api/web/auth/login
    is the only unauthenticated endpoint

v1 API (
/api/v1/*
)

  • Uses
    verify_key
    for 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

RouterAuth MethodConsumerFrontend Calls?
/api/web/*
Session token (
verify_session
)
Web frontendYES
/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) or
    verify_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)