Skip to content

refactor: consolidate API routes in application/api/v1/ #2

@rorybyrne

Description

@rorybyrne

Summary

Consolidate all HTTP API routes into application/api/v1/ with thin routers that delegate to domain Command/Query handlers. Currently, routers are scattered across domain/*/api/rest/ folders, which violates the principle that domains should be protocol-agnostic.

Current State

osa/
  domain/
    search/api/rest/routes.py      # Router inside domain
    deposition/api/rest/routes.py  # Router inside domain
  application/api/rest/
    routes/health.py               # Application-level routes

Proposed Structure

osa/
  application/
    api/
      v1/
        routes/
          __init__.py        # Combines all routers
          health.py          # Health checks
          depositions.py     # HTTP → Command/Query handlers
          records.py
          search.py
          validation.py
        errors.py            # Centralized error transformation (DomainError → HTTPException)
  domain/
    deposition/
      command/
        create.py            # CreateDeposition, CreateDepositionHandler
        submit.py            # SubmitDeposition, SubmitDepositionHandler
      query/
        get.py               # GetDeposition, GetDepositionHandler
      service/
        deposition.py        # DepositionService (used by handlers)
      # NO api/ folder - domains are protocol-agnostic

Thin Router Pattern

Routers should only handle:

  1. Request parsing (HTTP → Command/Query DTO)
  2. Handler invocation via Dishka
  3. Response formatting (Result → HTTP response)
  4. Error transformation (DomainError → HTTPException)

Example:

# application/api/v1/routes/depositions.py
from dishka.integrations.fastapi import FromDishka
from fastapi import APIRouter
from osa.domain.deposition.command.create import CreateDeposition, CreateDepositionHandler
from osa.application.api.v1.errors import map_domain_error

router = APIRouter(prefix="/depositions", tags=["depositions"])

@router.post("/")
async def create_deposition(
    request: CreateDepositionRequest,
    handler: FromDishka[CreateDepositionHandler],
) -> DepositionResponse:
    try:
        cmd = CreateDeposition(metadata=request.metadata)
        result = await handler.run(cmd)
        return DepositionResponse(srn=str(result.srn))
    except DomainError as e:
        raise map_domain_error(e)

Tasks

  • Create application/api/v1/errors.py with map_domain_error() function
  • Move domain/search/api/rest/routes.pyapplication/api/v1/routes/search.py
  • Move domain/deposition/api/rest/routes.pyapplication/api/v1/routes/depositions.py
  • Remove empty domain/*/api/ directories
  • Update application/api/rest/app.py to import from new locations
  • Update CLAUDE.md to reflect new structure

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions