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:

FastAPIDjango REST Framework
DialectfastapiDialect()djangoDialect()
Schema/openapi.json — built in/api/schema/?format=json — drf-spectacular
Types come fromPydantic modelsSerializers
Pageskip / limit, or page / size with fastapi-paginationpage / page_size (PageNumberPagination), or limit / offset
Total{ items, total }; a bare array has none{ count, results }; a bare array has none
Sortorder_by=-created_atordering=-created_at
Record/items/{id}/articles/{id}/ — the trailing slash is DRF's
Validation error422 { detail: [{ loc, msg }] }400 { field: ["message"] }, { detail }
CORSCORSMiddlewaredjango-cors-headers
TokenOAuth2PasswordBearer + PyJWTdjangorestframework-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:8000

Django

npx react-resource-view init my-admin -b django -u http://localhost:8000

The 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
# 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:

settings.py · urls.py
# 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 not
src/resources/items.ts
import 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

main.py
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

settings.py
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

main.py
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_methods=["*"],
    allow_headers=["*"],  # Authorization included
)

Django

settings.py
# 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

main.py
# 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

settings.py · urls.py
# 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
// 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 401 is 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 localStorage until you add one.