DRF inspired REST Framework for FastAPI.
FastREST lets you build async REST APIs using the patterns you already know from DRF — serializers, viewsets, routers, permissions — running on FastAPI with Pydantic validation and auto-generated OpenAPI docs.
pip install fastrest
Status: Alpha (0.1.1). The core API is stable for serializers, viewsets, routers, pagination, and filtering. Authentication backends are coming in future releases.
If you've used Django REST Framework, you know how productive it is. But DRF is synchronous and tied to Django's ORM. FastREST gives you the same developer experience on a modern async stack:
| DRF | FastREST | |
|---|---|---|
| Framework | Django | FastAPI |
| ORM | Django ORM | SQLAlchemy (async) |
| Validation | DRF fields | DRF fields + Pydantic |
| Async | No | Native async/await |
| OpenAPI | Via drf-spectacular | Built-in (per-method routes) |
| Type hints | Optional | First-class |
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class Author(Base):
__tablename__ = "authors"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False)
bio = Column(String(1000))
is_active = Column(Boolean, default=True)from fastrest.serializers import ModelSerializer
class AuthorSerializer(ModelSerializer):
class Meta:
model = Author
fields = ["id", "name", "bio", "is_active"]
read_only_fields = ["id"]from fastrest.viewsets import ModelViewSet
class AuthorViewSet(ModelViewSet):
queryset = Author
serializer_class = AuthorSerializerfrom fastapi import FastAPI
from fastrest.routers import DefaultRouter
router = DefaultRouter()
router.register("authors", AuthorViewSet, basename="author")
app = FastAPI(title="My API")
app.include_router(router.urls, prefix="/api")That's it. You now have:
GET /api/authors— List all authorsPOST /api/authors— Create an author (201)GET /api/authors/{pk}— Retrieve an authorPUT /api/authors/{pk}— Update an authorPATCH /api/authors/{pk}— Partial updateDELETE /api/authors/{pk}— Delete an author (204)GET /api/— API root listing all resourcesGET /docs— Interactive Swagger UI with typed schemasGET /redoc— ReDoc documentation
ModelSerializer auto-generates fields from your SQLAlchemy model, just like DRF:
from fastrest.serializers import ModelSerializer
from fastrest.fields import FloatField
from fastrest.exceptions import ValidationError
class BookSerializer(ModelSerializer):
# Override auto-generated fields
price = FloatField(min_value=0.01)
class Meta:
model = Book
fields = ["id", "title", "isbn", "price", "author_id"]
read_only_fields = ["id"]
# Per-field validation hooks
def validate_isbn(self, value):
if value and len(value) not in (10, 13):
raise ValidationError("ISBN must be 10 or 13 characters.")
return valueSupported fields: CharField, IntegerField, FloatField, BooleanField, DecimalField, DateTimeField, DateField, TimeField, UUIDField, EmailField, URLField, SlugField, ListField, DictField, JSONField, SerializerMethodField, and more.
from fastrest.viewsets import ModelViewSet, ReadOnlyModelViewSet
class BookViewSet(ModelViewSet):
queryset = Book
serializer_class = BookSerializer
# Switch serializer based on action
def get_serializer_class(self):
if self.action == "retrieve":
return BookDetailSerializer
return BookSerializerAdd custom endpoints to viewsets with the @action decorator:
from fastrest.decorators import action
from fastrest.response import Response
class BookViewSet(ModelViewSet):
queryset = Book
serializer_class = BookSerializer
@action(methods=["get"], detail=False, url_path="in-stock")
async def in_stock(self, request, **kwargs):
"""GET /api/books/in-stock — List only in-stock books."""
books = await self.adapter.filter_queryset(
Book, self.get_session(), in_stock=True
)
serializer = self.get_serializer(books, many=True)
return Response(data=serializer.data)
@action(methods=["post"], detail=True, url_path="toggle-stock")
async def toggle_stock(self, request, **kwargs):
"""POST /api/books/{pk}/toggle-stock — Toggle in_stock flag."""
book = await self.get_object()
session = self.get_session()
await self.adapter.update(book, session, in_stock=not book.in_stock)
serializer = self.get_serializer(book)
return Response(data=serializer.data)Add pagination to any viewset:
from fastrest.pagination import PageNumberPagination
class BookPagination(PageNumberPagination):
page_size = 20
max_page_size = 100
class BookViewSet(ModelViewSet):
queryset = Book
serializer_class = BookSerializer
pagination_class = BookPaginationPaginated list responses return an envelope:
{
"count": 42,
"next": "?page=2&page_size=20",
"previous": null,
"results": [...]
}Also available: LimitOffsetPagination with ?limit=20&offset=0.
Add search and ordering with filter backends:
from fastrest.filters import SearchFilter, OrderingFilter
class BookViewSet(ModelViewSet):
queryset = Book
serializer_class = BookSerializer
pagination_class = BookPagination
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["title", "description", "isbn"]
ordering_fields = ["title", "price"]
ordering = ["title"] # default orderingGET /api/books?search=django— case-insensitive search acrosssearch_fieldsGET /api/books?ordering=-price— sort by price descendingGET /api/books?ordering=title,price— multi-field sort- All query parameters appear automatically in OpenAPI
/docs
Composable permission classes with &, |, ~ operators:
from fastrest.permissions import BasePermission, IsAuthenticated
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner_id == request.user.id
class ArticleViewSet(ModelViewSet):
queryset = Article
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticated & IsOwner]Built-in: AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly.
from fastrest.routers import DefaultRouter, SimpleRouter
# DefaultRouter adds an API root view at /
router = DefaultRouter()
router.register("authors", AuthorViewSet, basename="author")
router.register("books", BookViewSet, basename="book")
# SimpleRouter without the root view
router = SimpleRouter()Each HTTP method gets its own OpenAPI route with:
- Correct status codes (201 for create, 204 for delete)
- Typed
pk: intpath parameters - Request/response Pydantic schemas auto-generated from serializers
- Tag-based grouping by resource
- Unique operation IDs
Three levels of validation, same as DRF:
class ReviewSerializer(ModelSerializer):
class Meta:
model = Review
fields = ["id", "book_id", "reviewer_name", "rating", "comment"]
# 1. Field-level: validate_{field_name}
def validate_rating(self, value):
if not (1 <= value <= 5):
raise ValidationError("Rating must be between 1 and 5.")
return value
# 2. Object-level: validate()
def validate(self, attrs):
if attrs.get("rating", 0) < 3 and not attrs.get("comment"):
raise ValidationError("Low ratings require a comment.")
return attrs
# 3. Field constraints via field kwargs
# e.g., CharField(max_length=500), IntegerField(min_value=1)Built-in async test client:
import pytest
from fastrest.test import APIClient
@pytest.fixture
def client(app):
return APIClient(app)
@pytest.mark.asyncio
async def test_create_author(client):
resp = await client.post("/api/authors", json={
"name": "Ursula K. Le Guin",
"bio": "Science fiction author",
})
assert resp.status_code == 201
assert resp.json()["name"] == "Ursula K. Le Guin"
@pytest.mark.asyncio
async def test_list_authors(client):
resp = await client.get("/api/authors")
assert resp.status_code == 200
assert isinstance(resp.json(), list)For when you don't need the full viewset:
from fastrest.generics import (
ListCreateAPIView,
RetrieveUpdateDestroyAPIView,
)
class AuthorList(ListCreateAPIView):
queryset = Author
serializer_class = AuthorSerializer
class AuthorDetail(RetrieveUpdateDestroyAPIView):
queryset = Author
serializer_class = AuthorSerializerAvailable: CreateAPIView, ListAPIView, RetrieveAPIView, DestroyAPIView, UpdateAPIView, ListCreateAPIView, RetrieveUpdateAPIView, RetrieveDestroyAPIView, RetrieveUpdateDestroyAPIView.
See the fastrest-example repo for a complete bookstore API with authors, books, tags, and reviews.
FastREST implements the core DRF public API. If you've used DRF, you already know FastREST:
| DRF | FastREST | Status |
|---|---|---|
ModelSerializer |
ModelSerializer |
Done |
ModelViewSet |
ModelViewSet |
Done |
ReadOnlyModelViewSet |
ReadOnlyModelViewSet |
Done |
DefaultRouter |
DefaultRouter |
Done |
@action |
@action |
Done |
permission_classes |
permission_classes |
Done |
ValidationError |
ValidationError |
Done |
| Field library | Field library | Done |
APIClient (test) |
APIClient (test) |
Done |
| Pagination | PageNumberPagination, LimitOffsetPagination |
Done |
| Filtering/Search | SearchFilter, OrderingFilter |
Done |
| Authentication backends | — | Planned |
| Throttling | — | Planned |
| Content negotiation | — | Planned |
- Python 3.10+
- FastAPI 0.100+
- Pydantic 2.0+
- SQLAlchemy 2.0+ (async)
BSD 3-Clause. See LICENSE.
