Skip to main content

API Development

Maybern uses class-based views with strong typing for all API endpoints. This guide covers the patterns and best practices for building APIs.

API View Structure

APIs should be thin and delegate complex logic to services:
from server.apps.core.api.views import GetAPIView, PostAPIView
from server.apps.core.api.operation_ids import OperationIds
from server.apps.core.dataclasses.api_data import URLParams
from server.apps.core.dataclasses.request_ctx import RequestCtx
from server.apps.core.decorators.api import api_get, api_post

from server.apps.my_app.public.dataclasses import RequestData, ResponseData
from server.apps.my_app.public.services import MyAppService


class MyEntityAPI(GetAPIView[ParamType, ResponseType], PostAPIView[RequestType, ParamType, ResponseType]):
    """
    API dedicated to my entity operations.
    Each API class should have a clear responsibility and documentation.
    """

    @api_get(
        operation_id=OperationIds.MY_APP.value.GET_MY_ENTITY,
        response=ResponseData,
        description="Gets my entity data.",
        parameters=ParamType,
    )
    def get(
        self,
        *,
        url_params: URLParams,
        ctx: RequestCtx,
        param_data: ParamType,
    ) -> ResponseData:
        """
        GET request implementation with clear documentation.
        """
        return MyAppService.get_entity(
            ctx=ctx,
            entity_id=url_params["entity_id"],
            include_details=param_data.include_details,
        )

    @api_post(
        operation_id=OperationIds.MY_APP.value.CREATE_MY_ENTITY,
        request=RequestData,
        response=ResponseData,
        description="Creates a new entity.",
    )
    def post(
        self,
        *,
        url_params: URLParams,
        ctx: RequestCtx,
        request_data: RequestData,
        param_data: ParamType,
    ) -> ResponseData:
        """
        POST request to create an entity.
        """
        return MyAppService.create_entity(
            ctx=ctx,
            parent_id=url_params["parent_id"],
            entity_data=request_data,
        )

Key Components

API Decorators

The @api_get, @api_post, @api_put, @api_patch, and @api_delete decorators handle:
  • Request validation
  • Response serialization
  • OpenAPI schema generation
  • Error handling
@api_get(
    operation_id=OperationIds.MY_APP.value.GET_ENTITY,  # Unique operation ID
    response=ResponseData,                               # Response dataclass
    description="Gets entity details.",                  # OpenAPI description
    parameters=ParamType,                                # Query parameter dataclass
)

Operation IDs

Every endpoint must have a unique operation ID defined in OperationIds:
# server/apps/core/api/operation_ids.py
class MyAppOperationIds(StrEnum):
    GET_MY_ENTITY = "getMyEntity"
    CREATE_MY_ENTITY = "createMyEntity"
    UPDATE_MY_ENTITY = "updateMyEntity"
    DELETE_MY_ENTITY = "deleteMyEntity"
Operation IDs are used for:
  • OpenAPI schema generation
  • Frontend client generation
  • API documentation
  • Logging and monitoring

Request Context

The RequestCtx object contains request-scoped information:
@dataclass
class RequestCtx:
    customer_id: MUUID        # Current customer/tenant
    user_id: MUUID            # Authenticated user
    request_source: RequestSource  # Frontend, API, etc.
    feature_flags: list[str]  # Active feature flags
    # ... other context
Access it in any service via the ctx parameter.

URL Parameters

URL path parameters are passed via URLParams:
def get(self, *, url_params: URLParams, ctx: RequestCtx, ...):
    entity_id = url_params["entity_id"]  # From URL path

API Helper Functions

Extract complex response building logic into helper functions:
# api/helpers/build_response.py
def build_entity_response(
    *,
    ctx: RequestCtx,
    entity_data: EntityData,
    include_details: bool,
) -> EntityResponse:
    """
    Builds a standardized entity response.
    Helper functions should have clear purpose and documentation.
    """
    response = EntityResponse(
        id=entity_data.id,
        name=entity_data.name,
        # ... base fields
    )
    
    if include_details:
        response.details = get_entity_details(ctx=ctx, entity_id=entity_data.id)
    
    return response

Request/Response Dataclasses

Use typed dataclasses for all request and response structures:
# api/dataclasses.py
from dataclasses import dataclass
from typing import Optional
from server.apps.core.types.muuid import MUUID

@dataclass
class GetEntityParams:
    """Query parameters for getting an entity."""
    include_details: bool = False
    include_history: bool = False

@dataclass
class CreateEntityRequest:
    """Request body for creating an entity."""
    name: str
    description: str = ""
    parent_id: Optional[MUUID] = None

@dataclass
class EntityResponse:
    """Response for entity endpoints."""
    id: MUUID
    name: str
    description: str
    created_at: datetime

Testing APIs

API tests should mock the service layer:
@pytest.mark.django_db
class TestMyEntityAPI:
    @pytest.fixture
    def view(self):
        return MyEntityAPI.as_view()

    @pytest.fixture
    def mock_service(self, mocker):
        return mocker.patch("server.apps.my_app.api.my_entity.MyAppService")

    def test_get(
        self,
        mock_service,
        ctx_no_db,
        view,
        request_factory,
    ):
        # Arrange
        entity_id = MUUID("ENTY_00000000000000000000001")
        mock_service.get_entity.return_value = EntityResponse(
            id=entity_id, 
            name="Test"
        )
        request = request_factory.get(f"/api/entities/{entity_id}/")
        
        # Act
        response = view(request, entity_id=str(entity_id))
        
        # Assert
        assert response.status_code == 200
        response_data = json.loads(response.content)
        assert response_data["id"] == str(entity_id)
        mock_service.get_entity.assert_called_once()

Best Practices

APIs should only handle:
  • Request parsing
  • Response formatting
  • Delegating to services
All business logic belongs in services.
Always define typed dataclasses for:
  • Request bodies
  • Query parameters
  • Response data
This enables:
  • Type checking
  • OpenAPI schema generation
  • Frontend client generation
Every endpoint needs:
  • Operation ID
  • Description
  • Parameter documentation
  • Response documentation
Follow these conventions:
  • GET /entities/listEntities
  • GET /entities/{id}/getEntity
  • POST /entities/createEntity
  • PUT /entities/{id}/updateEntity
  • DELETE /entities/{id}/deleteEntity

Next Steps