react-resource-view
For Python developers
FastAPI and Django REST Framework side by side: the starter, the schema, types from your models, pagination, CORS and tokens.
Side by side#
You write the API in Python; the back office is React. react-resource-view sits between the two: it reads the OpenAPI schema your framework already publishes, types the front end from your Pydantic models or serializers, and draws the list, detail, create, edit and delete screens of each resource from one declaration. FastAPI and Django REST Framework each have a dialect:
| FastAPI | Django REST Framework | |
|---|---|---|
| Dialect | fastapiDialect() | djangoDialect() |
| Schema | /openapi.json — built in | /api/schema/?format=json — drf-spectacular |
| Types come from | Pydantic models | Serializers |
| Page | skip / limit, or page / size with fastapi-pagination | page / page_size (PageNumberPagination), or limit / offset |
| Total | { items, total }; a bare array has none | { count, results }; a bare array has none |
| Sort | order_by=-created_at | ordering=-created_at |
| Record | /items/{id} | /articles/{id}/ — the trailing slash is DRF's |
| Validation error | 422 { detail: [{ loc, msg }] } | 400 { field: ["message"] }, { detail } |
| CORS | CORSMiddleware | django-cors-headers |
| Token | OAuth2PasswordBearer + PyJWT | djangorestframework-simplejwt |
Each is detailed in Backends & dialects.
One command#
The starter writes a Vite and React project with the dialect wired, the API's URL in .env and the commands that dump the schema:
FastAPI
npx react-resource-view init my-admin -b fastapi -u http://localhost:8000Django
npx react-resource-view init my-admin -b django -u http://localhost:8000The per-technology setups are in Start a project: FastAPI and Django.
The OpenAPI schema#
FastAPI
FastAPI serves /openapi.json out of the box, generated from your routes and Pydantic models.
# main.py — nothing to add: GET /openapi.json is already there
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
id: int
title: str
status: Literal["draft", "published"]
app = FastAPI()Django
DRF publishes nothing by itself. drf-spectacular reads the viewsets and serializers and serves the schema as JSON:
# pip install drf-spectacular
# settings.py
INSTALLED_APPS += ["drf_spectacular"]
REST_FRAMEWORK = {"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema"}
# urls.py — GET /api/schema/?format=json
from drf_spectacular.views import SpectacularAPIView
urlpatterns += [path("api/schema/", SpectacularAPIView.as_view())]Types from your models#
The starter adds two scripts that run api-dumper on the schema. A Pydantic model or a serializer becomes an entry of components["schemas"], named after its class, and the API stays the single source of truth:
npm run api-schema # api-dumper reads the schema, writes src/api-schema/
npm run api-schema:check # in the CI: fails when the models changed and the types did notimport type { components } from "@/api-schema/api-schema"
// FastAPI: the Pydantic model Item. Django: the serializer ArticleSerializer → Article.
type Item = components["schemas"]["Item"] & { "@id": string }
export const items = createViewResource<Item>("items", { … })The records carry no @id, so the resource intersects the schema with one. Types from OpenAPI has the rest, the enums and the Pydantic -Input / -Output pairs included.
Pagination#
A bare list has no total, so the pagination hides itself. Use a paginator that counts:
FastAPI — fastapi-pagination
from fastapi_pagination import Page, add_pagination, paginate
@app.get("/items", response_model=Page[Item])
def list_items():
return paginate(repository.all()) # → { items, total, page, size, pages }
add_pagination(app)
# front: dialect: fastapiDialect({ pagination: "page-size" })Django — PageNumberPagination
from rest_framework.pagination import PageNumberPagination
class PageSizePagination(PageNumberPagination):
page_size = 30
page_size_query_param = "page_size" # ?page=2&page_size=10
REST_FRAMEWORK["DEFAULT_PAGINATION_CLASS"] = "core.pagination.PageSizePagination"
# → { count, next, previous, results }
# front: dialect: djangoDialect() (LimitOffsetPagination: { pagination: "limit-offset" })page_size_query_param
DRF ignores ?page_size= unless the paginator sets page_size_query_param. Without it the list keeps the server's page size, and the view's rows-per-page choice has no effect.
CORS#
The views fetch from the browser, on another origin while you develop (http://localhost:5173). Without CORS the list stays empty and the console says why.
FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"], # Authorization included
)Django
# pip install django-cors-headers
INSTALLED_APPS += ["corsheaders"]
MIDDLEWARE.insert(0, "corsheaders.middleware.CorsMiddleware") # before CommonMiddleware
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]Token authentication#
Both frameworks hand out a JWT in exchange for credentials. The front end keeps it and gives it to getAuthToken, which the library sends as Authorization: Bearer … with every request:
FastAPI
# POST /token { username, password } → { access_token, token_type }
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
oauth2 = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items", response_model=Page[Item])
def list_items(token: str = Depends(oauth2)): # decode the JWT with PyJWT
...Django
# pip install djangorestframework-simplejwt
# POST /api/token/ { username, password } → { access, refresh }
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = [
"rest_framework_simplejwt.authentication.JWTAuthentication",
]
REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = [
"rest_framework.permissions.IsAuthenticated",
]
urlpatterns += [path("api/token/", TokenObtainPairView.as_view())]On the React side
// src/setup.ts — the same for both
function getAuthToken(): string | undefined {
return localStorage.getItem("token") ?? undefined // sent as Authorization: Bearer …
}
configureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: djangoDialect(), // or fastapiDialect()
})- A
401is the API saying the token is missing or expired: Authentication shows how a scope sends the reader to a sign-in page. - The sign-in itself — storing the token, refreshing it — is yours, or react-jwt-session's. The starter reads
localStorageuntil you add one.