Fast Actions
Fast Actions generates opinionated, class-based action routers for FastAPI.
It is intended for typed, action-oriented RPC APIs where every operation:
- uses
POST; - accepts exactly one Pydantic request object;
- returns exactly one Pydantic response object;
- has no path, query, header, or cookie parameters in its action signature; and
- receives shared FastAPI dependencies through controller fields.
Fast Actions only generates endpoints. It does not replace the FastAPI application, install middleware, register global exception handlers, or change dependency injection and request validation behavior.
Installation
pip install fast-actions
Python 3.12 or newer is required.
Example
import typing as t
from fastapi import FastAPI
from pydantic import BaseModel
from fast_actions import Controller, Depends, Error, errors
class CreateUserRequest(BaseModel):
email: str
class CreateUserResponse(BaseModel):
user_id: str
class EmailAlreadyExists(Error):
pass
async def provide_user_service() -> 'UserService':
...
class Users(Controller):
users: t.Annotated['UserService', Depends(provide_user_service)]
@errors(EmailAlreadyExists)
async def create(self, request: CreateUserRequest) -> CreateUserResponse:
if await self.users.email_exists(request.email):
message = 'A user already exists with this email.'
raise EmailAlreadyExists(message)
user = await self.users.create(request.email)
return CreateUserResponse(user_id=user.id)
app = FastAPI()
Users.mount_on(app, '/users')
This defines POST /users/create.
mount_on() is a convenience for regular FastAPI router inclusion. The following forms are
equivalent:
Users.mount_on(app, '/users')
app.include_router(Users.to_router('/users'))
Both FastAPI and APIRouter instances can be mounting targets.
Actions
Every public asynchronous instance method declared on a controller is an action. Method names are converted from snake case to kebab case:
class Users(Controller):
async def reset_password(
self, request: ResetPasswordRequest
) -> ResetPasswordResponse:
...
This defines POST /users/reset-password when mounted on /users.
The method named exactly _ defines an action at the controller mount path itself:
class Users(Controller):
async def _(self, request: ListUsersRequest) -> ListUsersResponse:
...
This defines POST /users. Other names beginning with _ are private and are not registered, so
they can be used for controller helpers.
Action methods must:
- be asynchronous instance methods;
- accept exactly
selfand one required request argument; and - annotate the request and response with concrete Pydantic
BaseModelsubclasses orNone.
Invalid controller definitions raise ControllerDefinitionError when converted to a router.
Inherited controller actions are included. Action names that normalize to the same route are
rejected rather than being registered ambiguously.
Dependencies
Controller dependencies use Annotated and Depends without a constructor:
class Users(Controller):
users: t.Annotated[UserService, Depends(provide_user_service)]
actor: t.Annotated[Actor, Depends(authenticate)]
Fast Actions creates one controller instance per request and assigns the resolved dependencies to
its fields. Controllers cannot define __init__ or __new__, and action methods cannot declare
additional dependencies.
All controller dependencies run for every action on that controller. Split a controller when its actions need materially different dependencies or authentication policies.
Depends is re-exported by fast_actions; it is FastAPI's normal dependency marker, so dependency
overrides and dependency cleanup continue to work normally.
Empty Objects
Annotate a request or response as None to represent an empty JSON object:
class Cache(Controller):
async def clear(self, request: None) -> None:
...
The client must send {}, the action receives None, the action returns None, and the HTTP
response is {}. Extra request properties are rejected by FastAPI validation.
Errors
Raise Error from an action to return a JSON error response:
class PermissionDenied(Error):
@classmethod
def get_status(cls) -> int:
return 403
raise PermissionDenied('You cannot perform this action.')
The response is:
{
"code": "PERMISSION_DENIED",
"message": "You cannot perform this action."
}
Error.get_code() derives the stable code from the class name. Renaming an error class is therefore
an API-breaking change. Error.get_status() defaults to 400 and may return any status from 400
through 599 except 422, which remains reserved for FastAPI request and dependency validation.
Declare errors for OpenAPI with the metadata-only @errors decorator:
@errors(EmailAlreadyExists, PermissionDenied)
async def create(self, request: CreateUserRequest) -> CreateUserResponse:
...
An undeclared Error raised by the action is still returned using its code, message, and status; it
is simply absent from OpenAPI.
Fast Actions catches Error only around the controller method call. An Error raised by a
dependency is not converted. FastAPI HTTPException, request validation, dependency validation,
response validation, and unexpected exceptions keep their normal FastAPI behavior. In particular,
invalid requests remain 422 responses.
Business Outcomes
Business outcomes that should all return 200 belong in the application's response model. Fast
Actions has no special result or detailed-error abstraction:
class Created(BaseModel):
user_id: str
class EmailUnavailable(BaseModel):
code: t.Literal['EMAIL_UNAVAILABLE']
message: str
class CreateUserResponse(BaseModel):
result: Created | EmailUnavailable
Return these models normally instead of raising Error.
OpenAPI Names
Fast Actions creates private request and response model copies for every action. The names are
derived from the path known when to_router() or mount_on() is called:
/users -> UsersRequest, UsersResponse
/users/create -> UsersCreateRequest, UsersCreateResponse
/api/v1/users/create -> ApiV1UsersCreateRequest, ApiV1UsersCreateResponse
Reusing the same application model in multiple actions still produces distinct OpenAPI components.
Prefixes added later when a parent router is included are intentionally not part of these names:
Users.mount_on(api_router, '/users')
app.include_router(api_router, prefix='/api/v1')
The final route is /api/v1/users, but its components are named UsersRequest and UsersResponse.
Scope
Fast Actions is a good fit for internal APIs, backend-for-frontend services, and APIs consumed by generated clients. POST-only reads do not have normal HTTP cache or safe-method semantics. File uploads, streaming responses, and transport-oriented APIs should use FastAPI routes directly.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fast_actions-0.3.0.tar.gz.
File metadata
- Download URL: fast_actions-0.3.0.tar.gz
- Upload date:
- Size: 12.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d197ecae74e53fdc6951d0fdafadccec6e8e0367c620c4312d62f0203bd558a
|
|
| MD5 |
698c31cd62079d186c04983669ed0c10
|
|
| BLAKE2b-256 |
2a5bf85298d810dcbc46611fb05776b05079df4cca732d4fc416ff0751090a4f
|
File details
Details for the file fast_actions-0.3.0-py3-none-any.whl.
File metadata
- Download URL: fast_actions-0.3.0-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0548040bdfb46363541564c2632c556c709df72f5975c4c5c1c9ead2ee4ac2d7
|
|
| MD5 |
2f5d961587d24da0453adb5b92be5ebc
|
|
| BLAKE2b-256 |
175a391fb5c9a0ad994e86e95ca47ad5ee677fc072eec1906d1d46ece649b456
|