4.2. Presentation Layer
The Presentation Layer is responsible for a specific way of exposing the application — the CLI today, or a web framework like Flask when that template is installed. It depends on the Application Layer’s use cases and schemas, and never the other way around.
4.2.1. What belongs here
CLI Presentations (
presentation/cli/) — formats use case output for the command line.Web-facing input validation and exception translation, when a web framework template is installed (
presentation/web/).
4.2.2. What to avoid here
Business logic. The Presentation Layer displays data and captures input; it never decides what that data means.
Direct database access or persistence. That’s the Application/Infrastructure layers’ job, reached only through a use case.
Heavy computation. Presentation should stay cheap — formatting and validation, not processing.
Tight coupling to a specific framework’s internals beyond what’s strictly needed to receive input and send output.
4.2.3. Naming convention: <Domain>CLIPresentation
A CLI presentation is a class named <Domain>CLIPresentation, constructed with the use case(s) it presents, with a nested class Printer(ComponentPrinter) (setting domain = "...") holding the actual @classmethod print methods. ComponentPrinter (infrastructure/utils/component_printer.py) wraps every print call to auto-translate its message when the locales template is installed — and is a no-op passthrough when it isn’t, so a presentation class never needs to check for that itself.
4.2.4. Example: a CLI Presentation
presentation/cli/app.py — every fresh project ships this one, backing the default runners/main/app.py runner:
from application.schemas.output.app import AppSchema
from application.use_cases.app.get import GetAppUseCase
from infrastructure.utils.component_printer import (
ComponentPrinter,
)
class AppCLIPresentation:
class Printer(ComponentPrinter):
domain = "app"
@classmethod
def welcome(cls, app_name: str):
message = cls.translate("Welcome to {app_name}")
cls.title(message.format(app_name=app_name))
@classmethod
def app_info(cls, app: AppSchema):
app_dict = app.model_dump()
cls.title("App Informations")
for name, value in app_dict.items():
cls.info(name, end=": ")
cls.print(value)
def __init__(self, app_use_case: GetAppUseCase) -> None:
self.app_use_case = app_use_case
def show_informations(self) -> None:
app = self.app_use_case.execute()
self.Printer.app_info(app)
def start(self) -> None:
app = self.app_use_case.execute()
self.Printer.welcome(app.name)
Each public method (show_informations, start) follows the same shape: call the use case’s execute(), then hand the result to the Printer. The Printer nested class never talks to the Application Layer directly — it only ever receives data that’s already been through a use case and a schema.