Skip to the content.

Implementation

Principles

Coding principles

Workflow

API views, views

  sequenceDiagram
    participant router
    participant view as API View / View 
    participant bus as messages bus 
    participant handler as query/command handler
    participant database 
    router->>view: request 
    view->>bus: query/command 
    Note right of view: create query/command by <br/>input serializers/forms 
    bus->>handler: query/command 
    handler->>database: CRUD operations
    database->>handler: data 
    handler->>bus: query/command result
    bus->>view: query/command result
    view->>router: response 
    Note left of view: create response by <br/>output serializers/templates

Examples

Query


@dataclass(frozen=True)
class QueryResult:  # query result
    """Posts list result."""

    instances: models.QuerySet


class Query(messages.BaseQuery[QueryResult]):   # inherit from BaseQuery 
    """Post list query."""

    author_id: int | None = None


class QueryHandler(messages.BaseQueryHandler[Query]):  # inherit from BaseQueryHandler
    """Posts list query handler."""

    def handle(self, query: Query) -> QueryResult:  # handle logic
        """Handler."""
        posts = Post.objects.filter(status=PostStatus.PUBLISHED)

        if query.author_id is not None:
            posts = posts.filter(author_id=query.author_id)

        posts = posts.order_by("-created_at")

        # returns query result
        return QueryResult(
            instances=posts,
        )

Command

@dataclass(frozen=True)
class CommandResult: # command result dto
    """Create post output dto."""

    instance: Post


class Command(messages.BaseCommand[CommandResult]): # inherit from BaseCommand
    """Create post command."""

    user_id: int | None
    title: str
    content: str  # noqa: WPS110
    status: PostStatus


class CommandHandler(messages.BaseCommandHandler[Command]):  # inherit from BaseCommandHandler
    """Create post."""

    def handle(self, command: Command) -> CommandResult:  # handle logic
        """Main logic here."""
        if not command.user_id:
            raise AccessDeniedApplicationError()

        user = User.objects.get(id=command.user_id)
        post = Post.objects.create(
            title=command.title,
            content=command.content,
            status=command.status,
            author=user,
        )
        # returns command result
        return CommandResult( 
            instance=post,
        )

Query API view

view.py

class _InputSerializer(serializers.Serializer):
    author = serializers.IntegerField(required=False)


# inherit from: 
# - BaseListQueryView for list instances queries: list posts, users,...  
# - BaseRetrieveQueryView for retrieve instance queries: get post, user,...
class View(BaseListQueryView):
    # query to call from view.
    query = posts.list.Query
    # sometimes you need some input parameters from user. To get it - set input serializer, and
    # you will get the parameters by calling "self.extract_input_dto()".
    input_serializer = _InputSerializer
    # output dto serializer. The serializer will get "instances" field from query result.
    output_serializer = PostCardSerializer

    # Every view must override the method to create query with required parameters.
    def create_query(self) -> queries.IQuery:
        input_dto = self.extract_input_dto()
        return self.query(
            author_id=input_dto.get("author"),
        )

urls.py

from apps.blogs.api.views import posts
from apps.core.api.routers import ActionsRouter

# action router is proxy to implement view per api url logic. 
router = ActionsRouter()
router.get("posts", posts.list.View.as_view())

urlpatterns = router.urls

Command API view

view.py

class _InputSerializer(serializers.Serializer):
    title = serializers.CharField()
    content = serializers.CharField()  # noqa: WPS110
    status = serializers.ChoiceField(choices=PostStatus)


# inherit from BaseCommandView
class View(BaseCommandView):
    # command to call from view. 
    command = posts.update.Command
    # sometimes you need some input parameters from user. To get it - set input serializer, and
    # you will get the parameters by calling "self.extract_input_dto()". 
    input_serializer = _InputSerializer
    # output dto serializer if need response to client.
    output_serializer = PostSerializer

    # Every view must override the method to create command with required parameters.
    def create_command(self) -> commands.ICommand:
        input_dto = self.extract_input_dto()
        return self.command(
            user_id=self.user.id if self.user else None,
            post_id=self.kwargs["pk"],
            title=input_dto["title"],
            content=input_dto["content"],
            status=input_dto["status"],
        )

    # Provides serializer data from command result.
    def get_output_serializer_instance(
            self,
            command_result: posts.create.CommandResult,
    ) -> Post:
        return command_result.instance

Url binding for the view is same as for query.

Query view

view.py

# inherit from: 
# - BaseListQueryView for list instances queries: list posts, users,...  
# - BaseRetrieveQueryView for retrieve instance queries: get post, user,...
class View(BaseListQueryView):
    # query to call from view. 
    query = posts.list.Query
    # template to render results.
    template_name = "posts/list.html"

    # Every view must override the method to create query with required parameters. 
    def create_query(self, request: HttpRequest) -> queries.IQuery:
        return self.query()

Add binding from url to view same as standard django mechanism.

Command view

view.py

class _Form(forms.ModelForm):
    class Meta:
        model = Post
        fields = ("title", "content", "status")


# inherit from BaseCommandView
class View(BaseCommandView):
    # command to call from view.
    command = posts_commands.update.Command
    # edit instance mostly requires additional html form to get data from the user. 
    # To get the instance to fill the form we need initial retrieve query. 
    initial_query = posts_queries.retrieve.Query
    # template for initial form.
    template_name = "posts/edit.html"
    # input data parser.
    form = _Form

    # build query for initial form.
    def create_initial_query(self, request: HttpRequest) -> queries.IQuery:
        return self.initial_query(
            user_id=request.user.id if request.user.is_authenticated else None,
            post_id=self.kwargs["id"],
            only_owner=True,
        )

    # build form to accept data from user. But ALL validation logic must be placed at command. 
    def create_initial_form(
            self,
            request: HttpRequest,
            initial_query_result: posts_queries.retrieve.QueryResult,
    ) -> forms.BaseForm:
        return self.form(instance=initial_query_result.instance)

    # create and fill the command with provided data
    def create_command(
            self,
            request: HttpRequest,
            form: _Form,
    ) -> commands.ICommand:
        return self.command(
            user_id=request.user.id if request.user.is_authenticated else None,
            post_id=self.kwargs["id"],
            title=form.data["title"],
            content=form.data["content"],
            status=form.data["status"],
        )

    # on command success we can do some additional logic: redirects to other page,... 
    def handle_command_success(
            self,
            request: HttpRequest,
            form: _Form,
            command_result: posts_commands.create.CommandResult,
    ) -> HttpResponse:
        return redirect("blogs:post_detail", command_result.instance.id)

Add url routing is same as standard django mechanism.

Future improvements