Quick Start¶
Restflow quickstart guide.
Prerequisites¶
- Django and DRF installed and configured.
- The relevant restflow Django apps in
INSTALLED_APPS. See Installation for the full setup.
Caching¶
The caching layer plugs into Django's cache framework. The
recommended setup is django-redis backed by redis (valkey, keydb, and
dragonfly also work). See the
Installation page for the install
line and the CACHES snippet.
Cache an expensive function¶
Wrap a function with @cache_result. The first call computes the
value and stores it in the cache; subsequent calls return the cached
value.
# app/services.py
from django.contrib.auth import get_user_model
from restflow.caching import (
KeyConstructor, ArgsKeyField, ConstantKeyField,
cache_result, InvalidationRule,
)
User = get_user_model()
class UserPayloadKey(KeyConstructor):
user = ArgsKeyField("user_id", partition=True)
version = ConstantKeyField("v", "1")
class Meta:
namespace = "users"
@cache_result(
key_constructor=UserPayloadKey,
ttl=300,
invalidates_on=[
InvalidationRule(
model=User,
field_mapping={"user_id": "id"},
watch_fields=["email", "username"],
),
],
)
def get_user_payload(user_id: int):
user = User.objects.get(pk=user_id)
return {
"id": user.id,
"username": user.username,
"email": user.email,
}
partition=True on user puts the user id in the cache key prefix.
watch_fields makes the rule fire only when email or username
actually changes on save. ttl=300 expires entries after five
minutes.
Note: To efficiently use watch_fields, make sure to pass
update_fields to model save method, e.g.:
Instead of:
Use the wrapped function¶
# app/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .services import get_user_payload
@api_view(["GET"])
def user_view(request, user_id):
return Response(get_user_payload(user_id))
The first request runs get_user_payload; the next requests within
minutes return the cached value. When the underlying user is
saved with a changed email, the registered InvalidationRule drops
the cached entry and the next request recomputes.
Surface the cache status¶
from rest_framework.decorators import api_view
from rest_framework.response import Response
from restflow.caching import set_response_cache_header
from .services import get_user_payload
@api_view(["GET"])
def user_view(request, user_id):
value, metadata = get_user_payload.get_with_metadata(user_id)
response = Response(value)
return set_response_cache_header(response, metadata)
The response carries X-Cache-status (HIT, MISS, STALE,
BYPASS, or REFRESH), X-Cached-at, and X-Cache-reset-at.
See the Caching Guide for the full API.
Filtering¶
FilterSet validates query parameters and applies filters to a
Django queryset.
Declare a FilterSet¶
# app/filters.py
from restflow.filters import (
FilterSet, StringField, IntegerField,
)
from .models import Product
class ProductFilterSet(FilterSet):
name = StringField(lookups=["icontains"])
price = IntegerField(lookups=["comparison"])
category: str
in_stock: bool
class Meta:
model = Product
order_fields = [
("price", "price"),
("name", "name"),
("created_at", "created_at"),
]
Plug it into a DRF view¶
# app/views.py
from restflow.views import AsyncListAPIView
from restflow.filters import RestflowFilterBackend
from .filters import ProductFilterSet
from .models import Product
from .serializers import ProductSerializer
class ProductListView(AsyncListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [RestflowFilterBackend]
filterset_class = ProductFilterSet
Regular AsyncAPIView¶
# app/views.py
from restflow.views import AsyncAPIView
from restflow.filters import RestflowFilterBackend
from .filters import ProductFilterSet
from .models import Product
from .serializers import ProductSerializer
class ProductAPIView(AsyncAPIView):
serializer_class = ProductSerializer
async def get(self, request):
qs = Product.objects.all()
qs = await ProductFilterSet(request).afilter_queryset(qs)
return await self.aserialized_response(qs, many=True)
Sample requests:
GET /api/products/?name__icontains=laptop&price__lte=1000&order_by=-price
GET /api/products/?category!=electronics
See the Filtering Guide for the full API.
Serializers¶
Type-annotated serializers resolve fields from Python annotations.
from restflow.serializers import (
Serializer, ModelSerializer, Field, Email,
)
class UserSerializer(Serializer):
name: str
age: int
email: Email
bio: str | None
role: str = Field(read_only=True)
class UserModelSerializer(ModelSerializer):
full_name: str
class Meta:
model = User
fields = ["id", "username"]
Optional[T] and T | None become allow_null=True.
Literal[...] becomes ChoiceField. list[T] becomes ListField.
The async surface (ais_valid, asave, acreate, aupdate) is
available on every serializer.
See the Serializers Guide for the resolution rules.
Authentication¶
JWT authentication ships with built-in obtain, refresh, and blacklist views and works on any Django auth backend.
Configure a signing key¶
# settings.py
from datetime import timedelta
RESTFLOW_SETTINGS = {
"JWT": {
"SIGNING_KEY": "change-me-in-production",
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
},
}
Mount the token views¶
# urls.py
from django.urls import path
from restflow.authentication.views import (
TokenObtainView, TokenRefreshView, TokenBlacklistView,
)
urlpatterns = [
path("auth/token/", TokenObtainView.as_view()),
path("auth/refresh/", TokenRefreshView.as_view()),
path("auth/blacklist/", TokenBlacklistView.as_view()),
]
Protect a view¶
from restflow.authentication import JWTAuthentication
from restflow.permissions import IsAuthenticated
from restflow.views import AsyncListAPIView
class ProductView(AsyncListAPIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
Sample requests:
curl -X POST http://localhost:8000/auth/token/ \
-H "Content-Type: application/json" \
-d '{"username": "khan", "password": "..."}'
# {"access": "eyJ...", "refresh": "eyJ..."}
curl http://localhost:8000/api/products/ \
-H "Authorization: Bearer eyJ..."
See the Authentication Guide for the configuration surface and the SimpleJWT adapter.
Permissions¶
Async compatible permission classes can be combined with &, |, and ~.
from restflow.permissions import (
IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly,
)
from restflow.views import AsyncRetrieveUpdateDestroyAPIView
class ProductDetail(AsyncRetrieveUpdateDestroyAPIView):
permission_classes = [
IsAuthenticated & (IsAdminUser | IsAuthenticatedOrReadOnly)
]
Custom async permissions implement ahas_permission:
from restflow.permissions import BasePermission
class IsOwner(BasePermission):
async def ahas_object_permission(self, request, view, obj):
return obj.owner_id == request.user.id
See the Permissions Guide for async hooks.
Views¶
Async views, generic views, mixins, and viewsets, all driven by an async dispatch loop.
from restflow.views import AsyncModelViewSet, ActionConfig
from restflow.permissions import IsAuthenticated, IsAdminUser
from restflow.pagination import FastPageNumberPagination
class ProductViewSet(AsyncModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [IsAuthenticated]
pagination_class = FastPageNumberPagination
action_configs = {
"destroy": ActionConfig(permission_classes=[IsAdminUser]),
}
See the Views Guide for the async pipeline.
Pagination¶
Async paginators that provides the apaginate_queryset() hook on async
views.
from restflow.pagination import (
PageNumberPagination, FastPageNumberPagination,
)
from restflow.views import AsyncListAPIView
class ProductView(AsyncListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
pagination_class = PageNumberPagination
class HugeProductView(AsyncListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
pagination_class = FastPageNumberPagination
FastPageNumberPagination skips the COUNT(*) query and is
appropriate when the table is large enough that counting is expensive.
See the Pagination Guide for selection criteria and tuning.
Throttling¶
Async throttles backed by Django's async cache.
from restflow.throttling import AnonRateThrottle, UserRateThrottle
from restflow.views import AsyncListAPIView
class ProductView(AsyncListAPIView):
throttle_classes = [AnonRateThrottle, UserRateThrottle]
# settings.py
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_RATES": {
"anon": "100/hour",
"user": "1000/hour",
},
}
See the Throttling Guide for cache-backend selection.
Responses¶
Stream large payloads with streaming responses.
from restflow.responses import StreamingJSONListResponse
async def products(request):
async def items():
async for row in Product.objects.all():
yield {"id": row.id, "name": row.name}
return StreamingJSONListResponse(items())
NDJSONResponse produces one JSON object per line. SSEResponse
produces Server-Sent Events.
See the Responses Guide for buffering and encoder customisation.
Exception handler¶
Render every error in a uniform format with a stable error code.
from restflow.exceptions import APIException, ErrorCode
class ProductLockedException(APIException):
code = ErrorCode.CONFLICT.value
status_code = 409
default_detail = "The product is locked for editing."
Every error becomes:
See the Exception handler Guide for the full code list and customisation hooks.
Spectacular¶
Generate OpenAPI schemas through drf-spectacular.
# settings.py
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "restflow.spectacular.RestflowAutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "Example API",
"VERSION": "1.0.0",
}
# urls.py
from drf_spectacular.views import (
SpectacularAPIView, SpectacularSwaggerView,
)
urlpatterns = [
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
]
See the Spectacular Guide for action-config and pagination resolution rules.
Testing¶
AsyncAPIClient and the four AsyncAPI*TestCase classes wire async
testing into Django's test runner.
from restflow.test import AsyncAPIClient, AsyncAPITestCase
class TestProducts(AsyncAPITestCase):
async def test_list(self):
client = AsyncAPIClient()
response = await client.get("/api/products/")
assert response.status_code == 200
force_authenticate(request, user=...) bypasses the authenticator
chain in unit tests.
See the Testing Guide for picking the right base class.