react-resource-view
Start a project
npx react-resource-view init: a back office on your API, its schema dumped into TypeScript.
One command#
The fastest way to see your API as a back office is to let the package write the application around it. Give it a directory, and it asks for the rest:
npx react-resource-view init my-adminIt asks which API the back office talks to, where that API runs, and where it publishes its OpenAPI schema — the last guessed from the backend, so pressing Enter is usually enough:
$ npx react-resource-view init my-admin
Which API does it talk to?
1. API Platform (JSON-LD / Hydra)
2. Spring Boot (springdoc-openapi)
3. Another REST API (OpenAPI)
4. Supabase (PostgREST)
5. Strapi v4 / v5
6. FastAPI
7. Django REST Framework (drf-spectacular)
Choice [1] 2
URL of the API? [http://localhost:8080]
URL of its OpenAPI schema? [http://localhost:8080/v3/api-docs]
Created my-admin — a back office on Spring Boot (springdoc-openapi).
cd my-admin
npm install
npm run api-schema # dumps http://localhost:8080/v3/api-docs into src/api-schema/
npm run devWith pnpm dlx, yarn dlx or bunx instead of npx, the commands it prints — and those it writes in the README and the Makefile — use that package manager.
The backends#
| Name | Type | Default | Description |
|---|---|---|---|
api-platform | jsonLdDialect() | /api/docs.jsonopenapi | API Platform. Also configures jsonld-api-client, and dumps with api-dumper's api-platform preset, CSV stripped. |
spring-boot | springDialect() | /v3/api-docs | Spring Boot with springdoc-openapi: a Page, a PagedModel or Spring Data REST's HAL. |
rest | fastapiDialect({ pagination: "page-size" }) | /openapi.json | Any REST API answering plain JSON, paged with ?page=&size=. If yours spells things otherwise, a dialect is one object. |
supabase | supabaseDialect({ apiKey }) | — | Supabase. Its schema is Swagger 2.0, which api-dumper refuses: the types come from supabase gen types instead. |
strapi | strapiDialect() | /api/openapi.json | Strapi v4 or v5. |
fastapi | fastapiDialect() | /openapi.json | FastAPI — the Pydantic models become the schemas. |
django | djangoDialect() | /api/schema/?format=json | Django REST Framework with drf-spectacular: the serializers become the schemas. |
The type column is the dialect src/setup.ts is written with; the default column is the schema's path, appended to the API's URL.
Per technology#
One command per backend, ready to paste. Each writes the same project; what changes is src/setup.ts, where the schema is read from, and what the backend itself has to allow.
API Platform
npx react-resource-view init my-admin -b api-platform -u https://localhostimport { configureClient } from "jsonld-api-client"
configureClient({ baseUrl: import.meta.env.VITE_API_URL, getAuthToken })
configureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: jsonLdDialect(),
})export default defineConfig({
source: "https://localhost/api/docs.jsonopenapi",
outDir: "src/api-schema",
preset: "api-platform",
strip: { contentTypes: ["text/csv"] },
})The schema is served by API Platform itself; the enums come out of it too (see Types from OpenAPI). On the API, let the back office's origin through:
# config/packages/nelmio_cors.yaml — the API Platform distribution ships it
nelmio_cors:
defaults:
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
# .env of the API
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'Spring Boot
npx react-resource-view init my-admin -b spring-boot -u http://localhost:8080configureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: springDialect(),
})export default defineConfig({
source: "http://localhost:8080/v3/api-docs",
outDir: "src/api-schema",
})springdoc-openapi publishes the schema at /v3/api-docs; a controller returning a Page is read as it stands — see the Spring Boot dialect. On the API:
<!-- pom.xml — publishes /v3/api-docs -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.0</version>
</dependency>
// The back office runs on another origin while developing.
@Configuration
class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**").allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE");
}
}Another REST API
npx react-resource-view init my-admin -b rest -u http://localhost:3000 -s http://localhost:3000/docs/openapi.jsonconfigureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
// Plain JSON, ?page=&size=, /items/{id}. Another shape: a dialect of your own.
dialect: fastapiDialect({ pagination: "page-size" }),
})export default defineConfig({
source: "http://localhost:3000/docs/openapi.json",
outDir: "src/api-schema",
})Express, NestJS, Laravel, Go… anything that publishes an OpenAPI 3 document. Pass its URL with --schema-url, since there is no convention to guess it from. If the API pages, sorts or nests its lists differently, replace the dialect with one of your own — the rest of the project stays.
Supabase
npx react-resource-view init my-admin -b supabase -u https://abcdefghijklmnop.supabase.coconfigureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: supabaseDialect({ apiKey: import.meta.env.VITE_SUPABASE_ANON_KEY }),
}){
"scripts": {
"api-schema": "mkdir -p src/api-schema && npx supabase gen types typescript --project-id \"$SUPABASE_PROJECT_REF\" --schema public > src/api-schema/database.types.ts"
}
}No api-dumper here: Supabase serves Swagger 2.0, so the types come from its own CLI. Fill VITE_SUPABASE_ANON_KEY and SUPABASE_PROJECT_REF in .env; the rows the back office can read are the ones your Row Level Security policies allow.
Strapi
npx react-resource-view init my-admin -b strapi -u http://localhost:1337configureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: strapiDialect(),
})export default defineConfig({
source: "http://localhost:1337/api/openapi.json",
outDir: "src/api-schema",
})Strapi 5 publishes its schema only when asked to — over HTTP, or as a file:
// config/server.js of the Strapi project — opt in to GET /api/openapi.json
module.exports = {
openapi: {
"content-api": { access: "public" },
},
}
# or generate it as a file, and point --schema-url at it
npm run strapi openapi generate -- --output ./openapi.jsonGive the role the back office signs in with the find, findOne, create, update and delete permissions on each collection type.
FastAPI
npx react-resource-view init my-admin -b fastapi -u http://localhost:8000configureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: fastapiDialect(),
})export default defineConfig({
source: "http://localhost:8000/openapi.json",
outDir: "src/api-schema",
})FastAPI serves /openapi.json out of the box, the Pydantic models as its schemas. On the API:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)Writing Python on both sides? The Python guide sets FastAPI and Django side by side.
Django
npx react-resource-view init my-admin -b django -u http://localhost:8000configureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: djangoDialect(),
})export default defineConfig({
source: "http://localhost:8000/api/schema/?format=json",
outDir: "src/api-schema",
})Django REST Framework publishes no schema by itself: drf-spectacular serves it at /api/schema/?format=json, the serializers as its schemas. The browser calls the API from another origin, so django-cors-headers has to let it through. On the API:
# pip install djangorestframework drf-spectacular django-cors-headers django-filter
# settings.py
INSTALLED_APPS += ["rest_framework", "drf_spectacular", "corsheaders", "django_filters"]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # before CommonMiddleware
"django.middleware.common.CommonMiddleware",
# …
]
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_PAGINATION_CLASS": "core.pagination.PageSizePagination",
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.OrderingFilter",
],
}
# core/pagination.py
from rest_framework.pagination import PageNumberPagination
class PageSizePagination(PageNumberPagination):
page_size = 30
page_size_query_param = "page_size" # what the dialect sends
# urls.py
from drf_spectacular.views import SpectacularAPIView
urlpatterns += [path("api/schema/", SpectacularAPIView.as_view())]See the Django dialect for what it reads and sends, and the Python guide for FastAPI and Django together.
What it writes#
A Vite, React, TypeScript and Tailwind project, with TanStack Router for the navigation and AdminLayout around one example resource:
my-admin/
├── api-dumper.config.ts # where the schema is, where the types go
├── Makefile # make dev, make api-schema, make api-schema-check
├── .env # VITE_API_URL — the API the views talk to
└── src/
├── setup.ts # the router, the API URL, the token, the dialect
├── scope.ts # the back office: its resources and its menu
├── resources/
│ └── books.ts # one resource — copy it for the next
├── App.tsx # reads the URL, renders the view it names
└── main.tsxEverything that depends on the backend is in src/setup.ts — the rest of the project reads the same whichever you picked:
// src/setup.ts — Spring Boot
configureApi({
baseUrl: import.meta.env.VITE_API_URL,
getAuthToken,
dialect: springDialect(),
})The browser calls the API
The views fetch from the reader's browser, so the API has to allow the origin the back office is served from — http://localhost:5173 while developing. Without CORS, the list stays empty and the console says why.
Dumping the API#
The starter comes with the commands that turn the API's schema into TypeScript, through api-dumper. The API has to be running:
npm run api-schema # reads the schema, writes src/api-schema/
npm run api-schema:check # in the CI: fails when src/api-schema is staleThe same, through the Makefile, which reads .env first:
make api-schema
make api-schema-checkThe example resource is typed by hand, so the project compiles before the API answers. Once the types are there, take them from the schema:
import type { components } from "@/api-schema/api-schema"
type Book = components["schemas"]["Book"] & { "@id": string }
export const books = createViewResource<Book>("books", { … })Types from OpenAPI goes through the rest: the enums, the API Platform preset, and when to regenerate.
On Supabase
# .env
VITE_SUPABASE_ANON_KEY=…
SUPABASE_PROJECT_REF=abcdefghijklmnop
# reads .env, then runs supabase gen types typescript
make api-schemaOptions#
Every question has a flag, so the starter also runs in a script:
npx react-resource-view init my-admin \
--backend spring-boot \
--api-url https://api.example.com \
--schema-url https://api.example.com/v3/api-docs \
--yes-b, --backend—api-platform,spring-boot,rest,supabase,strapi,fastapiordjango.-u, --api-url— the API's URL, written to.env.-s, --schema-url— the OpenAPI schema, URL or file; guessed when left out.--pm—pnpm,npm,yarnorbun, when the guess is wrong.--dry-runlists the files,--forcewrites into a directory that is not empty,--yesnever asks.