Custom Actions: Extend ViewSet Endpoints in FastAPI Mason¶
FastAPI Mason allows you to extend ViewSets by adding custom actions using the @action decorator. This lets you implement complex business logic and custom endpoints beyond standard CRUD operations, all with proper request context handling for seamless API functionality.
Important: Always add custom routes to your ViewSets using the
@action
decorator. This ensures that you have access to the request context viaself
, and all lifecycle hooks and permission checks are properly handled. Defining routes outside of@action
will break context and hook processing.
Actions allow you to add custom endpoints to your ViewSets beyond the standard CRUD operations. They're perfect for implementing business logic that doesn't fit into the standard create, read, update, delete pattern.
What are Actions?¶
Actions are custom methods in your ViewSet that are automatically registered as API endpoints. They're decorated with the @action
decorator and can handle various HTTP methods, accept parameters, and return custom responses.
Basic Action Usage¶
from fastapi_mason.decorators import action
@viewset(router)
class CompanyViewSet(ModelViewSet[Company]):
model = Company
read_schema = CompanyReadSchema
create_schema = CompanyCreateSchema
@action(methods=['GET'], detail=False, response_model=dict)
async def stats(self):
"""Get company statistics"""
total = await Company.all().count()
active = await Company.filter(is_active=True).count()
return {
"total": total,
"active": active,
"inactive": total - active
}
This creates a new endpoint: GET /companies/stats/
Overriding Standard Methods¶
You can override standard CRUD methods (such as list
, retrieve
, etc.) using the @action
decorator to add custom logic or parameters. This is useful when you need to extend or modify the default behavior of your ViewSet endpoints.
@action(methods=["GET"], response_model=PaginatedResponseDataWrapper[TaskReadSchema, PageNumberPagination])
async def list(
self,
pagination: PageNumberPagination = Depends(PageNumberPagination.build),
project_id: bool = Query(...),
):
"""Override list method"""
queryset = self.get_queryset().filter(project_id=project_id)
return await self.get_paginated_response(queryset=queryset, pagination=pagination)
@action(methods=["PUT"], detail=True)
async def update(self, item_id: int, data: TaskCreateSchema):
"""Override update method"""
task = await self.get_object(item_id)
await task.update_from_dict(data.model_dump(exclude_unset=True))
await task.save()
return task
Action Parameters¶
The @action
decorator accepts several parameters to customize the endpoint:
methods¶
Specify which HTTP methods the action accepts:
@action(methods=['GET']) # Default
async def get_data(self):
return {"data": "example"}
@action(methods=['POST'])
async def process_data(self):
return {"status": "processed"}
@action(methods=['GET', 'POST'])
async def flexible_endpoint(self):
if self.request.method == 'GET':
return {"data": "viewing"}
else:
return {"data": "processing"}
detail¶
Controls whether the action operates on a single instance or the collection:
# Collection action: /companies/stats/
@action(methods=['GET'], detail=False, response_model=int)
async def stats(self):
return await Company.all().count()
# Instance action: /companies/{item_id}/activate/
@action(methods=['POST'], detail=True, response_model=dict)
async def activate(self, item_id: int):
company = await self.get_object(item_id)
company.is_active = True
await company.save()
return {"message": "Company activated"}
path¶
Customize the URL path for the action:
@action(methods=['GET'], detail=False, path='company-statistics', response_model=dict)
async def stats(self):
return {"total": await Company.all().count()}
# Creates endpoint: /companies/company-statistics/
name¶
Set a custom name for the action (used internally):
@action(methods=['GET'], detail=False, name='company_stats', response_model=dict)
async def statistics(self):
return {"total": await Company.all().count()}
response_model¶
Specify the response model for OpenAPI documentation:
from pydantic import BaseModel
class StatsResponse(BaseModel):
total: int
active: int
inactive: int
@action(methods=['GET'], detail=False, response_model=StatsResponse)
async def stats(self):
total = await Company.all().count()
active = await Company.filter(is_active=True).count()
return StatsResponse(
total=total,
active=active,
inactive=total - active
)
Additional FastAPI Parameters¶
You can pass any additional FastAPI route parameters:
@action(
methods=['POST'],
detail=True,
status_code=202,
summary="Activate Company",
description="Activate a company by setting is_active to True",
tags=["company-management"]
)
async def activate(self, item_id: int):
company = await self.get_object(item_id)
company.is_active = True
await company.save()
return {"message": "Company activated"}
Actions provide a powerful way to extend your ViewSets with custom business logic while maintaining the clean, declarative style of FastAPI Mason.