How-To: React Admin Integration#

React-admin with ra-data-simple-rest expects a specific REST wire contract that differs from the default Restly contract in several ways: JSON-encoded sort and range parameters, a plain array response body, and a Content-Range header for pagination.

AsyncReactAdminView and ReactAdminView implement this contract. Switch to one of these view classes and ra-data-simple-rest works without a custom data provider.


Quick start#

Replace AsyncRestView with AsyncReactAdminView (or RestView with ReactAdminView for sync sessions):

import fastapi_restly as fr
from fastapi import FastAPI
from sqlalchemy.orm import Mapped

app = FastAPI()
fr.configure(async_database_url="sqlite+aiosqlite:///app.db")

class Product(fr.IDBase):
    name: Mapped[str]
    price: Mapped[float]

@fr.include_view(app)
class ProductView(fr.AsyncReactAdminView):
    prefix = "/products"
    model = Product

On the frontend, point ra-data-simple-rest at your API:

import { Admin, Resource } from "react-admin";
import simpleRestProvider from "ra-data-simple-rest";

const dataProvider = simpleRestProvider("http://localhost:8000");

export default () => (
  <Admin dataProvider={dataProvider}>
    <Resource name="products" />
  </Admin>
);

No custom data provider, no adapter layer.


Wire contract#

AsyncReactAdminView translates the ra-data-simple-rest query format to SQL and returns responses the provider expects.

List — GET /resource/#

Query parameter

Format

Example

sort

JSON [field, direction]

sort=["name","ASC"]

range

JSON [start, end] (inclusive)

range=[0,24]

filter

JSON object

filter={"name":"foo"} or filter={"id":[1,2,3]}

Response: a plain JSON array. The Content-Range header carries the total:

Content-Range: items 0-24/315

The id array form of filter ({"id": [1, 2, 3]}) is used by react-admin for getMany calls. It translates to WHERE id IN (1, 2, 3).

Other operations#

Method

Path

Purpose

Source

GET

/{id}

Get one — react-admin getOne

inherited from AsyncRestView

POST

/

Create — react-admin create

inherited from AsyncRestView

PUT

/{id}

Full update — react-admin update

added by AsyncReactAdminView

PATCH

/{id}

Partial update

inherited from AsyncRestView

DELETE

/{id}

Delete — react-admin delete

inherited from AsyncRestView

AsyncReactAdminView and ReactAdminView add a PUT /{id} endpoint because ra-data-simple-rest’s default update method issues a PUT request. The default PATCH /{id} is also kept available, so clients that prefer partial updates continue to work.

The PUT route delegates to the same perform_update handler as PATCH and accepts the view’s standard update_schema payload. Override perform_update (or replace the PUT route directly) if you need different write semantics for the two methods.



CORS setup#

Browsers block non-standard response headers by default. The Content-Range header must be explicitly exposed in your CORS configuration, otherwise the frontend cannot read the total and pagination breaks.

Add CORSMiddleware to your FastAPI app with Content-Range in expose_headers:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # your frontend origin
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["Content-Range"],
)

AsyncReactAdminView also sets Access-Control-Expose-Headers: Content-Range on every list response as a per-response fallback, but the middleware approach is more reliable and is the recommended setup for production.


Customization#

Set the default page size#

When the frontend does not send a range query parameter, Restly returns the first 25 rows. Set default_page_size on the view to choose a different default:

@fr.include_view(app)
class ProductView(fr.AsyncReactAdminView):
    prefix = "/products"
    model = Product
    default_page_size = 50

Change the Content-Range unit#

ra-data-simple-rest ignores the unit part of the header (it only parses the numbers), but if another consumer cares, override get_react_admin_range_unit:

class ProductView(fr.AsyncReactAdminView):
    prefix = "/products"
    model = Product

    def get_react_admin_range_unit(self) -> str:
        return "products"

Share the react-admin contract across multiple views#

Put shared customizations in a project base class and inherit your views from that class:

class ReactAdminBase(fr.AsyncReactAdminView):
    default_page_size = 100

    def get_react_admin_range_unit(self) -> str:
        return "items"

@fr.include_view(app)
class ProductView(ReactAdminBase):
    prefix = "/products"
    model = Product

@fr.include_view(app)
class CustomerView(ReactAdminBase):
    prefix = "/customers"
    model = Customer

Under the hood#

AsyncReactAdminView is a thin subclass of AsyncRestView built with the route replacement pattern. It replaces the listing route to change the list contract and adds a PUT /{id} route that delegates to the standard perform_update handler. All other generated routes (GET /{id}, POST /, PATCH /{id}, DELETE /{id}) and all perform_* handlers are inherited unchanged.

The shared parsing and response logic is an internal implementation detail of the concrete React Admin view classes.