Resource

Declare an entity, its CRUD views and its routes. Its Zod schema comes from defineResources.

A <Resource> declares one entity of your admin. Its name is the registry key and the route base (/<name>/*), and each view prop (list, edit, create, show) both registers a route and flags the capability that drives menus and action buttons.

Declaring resources

import { Resource } from "@chelone/react";
import { PostCreate, PostEdit, PostList, PostShow } from "./posts";
 
<Resource
  name="posts"
  list={PostList}
  edit={PostEdit}
  create={PostCreate}
  show={PostShow}
/>;

This registers four routes — /posts, /posts/create, /posts/:id, /posts/:id/show — and tells the sidebar the resource has a list. Because the resource also has edit and show views, the list rows link to them.

Point at a Zod schema

The other half of a resource is its schema. <Resource> has no schema prop: the schema comes from defineResources, keyed by the resource name. Hand the registry to <Admin resources={…}>. Because the resource carries a schema, its Create/Edit forms validate against it automatically — no per-input validate props:

src/resources/index.ts
import { defineResources } from "@chelone/core";
import { z } from "zod";
 
export const postSchema = z.object({
  id: z.number().optional(),
  title: z.string().min(1, "Title is required").max(120),
  body: z.string().min(1),
  author_id: z.number({ message: "Pick an author" }),
  published: z.boolean().optional(),
});
 
export const resources = defineResources({
  posts: { schema: postSchema },
});
src/App.tsx
import { Admin, Resource } from "@chelone/react";
import { dataProvider, queryClient } from "./providers";
import { PostEdit, PostList } from "./posts";
import { resources } from "./resources";
 
export function App() {
  return (
    <Admin
      dataProvider={dataProvider}
      queryClient={queryClient}
      resources={resources}
    >
      <Resource name="posts" list={PostList} edit={PostEdit} />
    </Admin>
  );
}

The schema is the single source of truth: it types the records and drives form validation.

To get List, Edit and Show pages from the same schema, use the guessers.

Labels and menus

recordRepresentation controls how a record names itself (a field name, a function, or a node) — used in titles and breadcrumbs. icon sets the menu icon, and options.group buckets the resource under a sidebar group.

import { Resource } from "@chelone/react";
import { FileText } from "lucide-react";
import { PostList } from "./posts";
 
<Resource
  name="posts"
  list={PostList}
  icon={FileText}
  recordRepresentation="title"
  options={{ group: "Content" }}
/>

Props

PropTypeDefaultDescription
childrenReactNode—Extra nested routes appended after the CRUD routes.
createResourceViewChild—The create view. Presence sets `hasCreate`.
editResourceViewChild—The edit view. Presence sets `hasEdit`.
hasCreateboolean—
hasEditboolean—
hasListboolean—Explicit route-flag overrides, OR-ed with component presence (e.g. a resource whose list view is registered elsewhere but should still show a "create" affordance).
hasShowboolean—
iconunknown—The menu / breadcrumb icon (a React component).
listResourceViewChild—The list view (component or element). Presence sets `hasList`.
namestring—The resource name — the registry key and the route base (`<name>/*`).
optionsRecord<string, unknown>—Open extension bag — the shadcn menu reads `options.group` (D9).
recordRepresentationunknown—How a record renders as a label (string field name, function, or node).
showResourceViewChild—The show view. Presence sets `hasShow`.

Tips

  • A view prop's presence sets the capability flag (list present → hasList); override with the explicit hasList/hasCreate/… props when a view lives elsewhere.
  • children on a <Resource> append extra nested routes after the CRUD routes.