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-admin

It 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 dev

With 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#

NameTypeDefaultDescription
api-platformjsonLdDialect()/api/docs.jsonopenapiAPI Platform. Also configures jsonld-api-client, and dumps with api-dumper's api-platform preset, CSV stripped.
spring-bootspringDialect()/v3/api-docsSpring Boot with springdoc-openapi: a Page, a PagedModel or Spring Data REST's HAL.
restfastapiDialect({ pagination: "page-size" })/openapi.jsonAny REST API answering plain JSON, paged with ?page=&size=. If yours spells things otherwise, a dialect is one object.
supabasesupabaseDialect({ apiKey })—Supabase. Its schema is Swagger 2.0, which api-dumper refuses: the types come from supabase gen types instead.
strapistrapiDialect()/api/openapi.jsonStrapi v4 or v5.
fastapifastapiDialect()/openapi.jsonFastAPI — the Pydantic models become the schemas.
djangodjangoDialect()/api/schema/?format=jsonDjango 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://localhost
src/setup.ts
import { 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(),
})
api-dumper.config.ts
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:8080
src/setup.ts
configureApi({
  baseUrl: import.meta.env.VITE_API_URL,
  getAuthToken,
  dialect: springDialect(),
})
api-dumper.config.ts
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 · CorsConfig.java
<!-- 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.json
src/setup.ts
configureApi({
  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" }),
})
api-dumper.config.ts
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.co
src/setup.ts
configureApi({
  baseUrl: import.meta.env.VITE_API_URL,
  getAuthToken,
  dialect: supabaseDialect({ apiKey: import.meta.env.VITE_SUPABASE_ANON_KEY }),
})
package.json
{
  "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:1337
src/setup.ts
configureApi({
  baseUrl: import.meta.env.VITE_API_URL,
  getAuthToken,
  dialect: strapiDialect(),
})
api-dumper.config.ts
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
// 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.json

Give 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:8000
src/setup.ts
configureApi({
  baseUrl: import.meta.env.VITE_API_URL,
  getAuthToken,
  dialect: fastapiDialect(),
})
api-dumper.config.ts
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:

main.py
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:8000
src/setup.ts
configureApi({
  baseUrl: import.meta.env.VITE_API_URL,
  getAuthToken,
  dialect: djangoDialect(),
})
api-dumper.config.ts
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:

settings.py · pagination.py · urls.py
# 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.tsx

Everything that depends on the backend is in src/setup.ts — the rest of the project reads the same whichever you picked:

src/setup.ts
// 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 stale

The same, through the Makefile, which reads .env first:

make api-schema
make api-schema-check

The example resource is typed by hand, so the project compiles before the API answers. Once the types are there, take them from the schema:

src/resources/books.ts
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-schema

Options#

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, fastapi or django.
  • -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, yarn or bun, when the guess is wrong.
  • --dry-run lists the files, --force writes into a directory that is not empty, --yes never asks.