1. Setup

Point Chelone at a Zod schema and mount an admin over an in-memory data provider.

Chelone turns a Zod schema into a typed, validated admin resource. You write the schema once; the Create and Edit forms validate against it automatically, and your records are typed end to end. This guide builds a small posts admin.

Point at a Zod schema

Describe your record with Zod, then register it with defineResources:

src/resources.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, "Body is required"),
  author_id: z.number({ message: "Pick an author" }),
  views: z.number().min(0).optional(),
  published: z.boolean().optional(),
});
 
export const resources = defineResources({
  posts: { schema: postSchema },
  authors: { schema: z.object({ id: z.number(), name: z.string() }) },
});

Because posts carries a schema, its forms get automatic validation — no per-field validate props. Zod ≥ 3.24 implements Standard Schema, so it plugs straight into Chelone's form-core validation seam.

A data source

Chelone talks to your API through a Data Provider adapter. For this guide we use the in-memory fakeDataProvider, seeded with a few posts and authors — no server required. It matches filters by equality, so a wrapper adds the q search used later in the guide. This search supports the page pagination used by <List>:

src/data-provider.ts
import { type DataProvider, fakeDataProvider } from "@chelone/core";
 
const baseDataProvider = fakeDataProvider({
  posts: [
    { id: 1, title: "On Computable Numbers", body: "…", author_id: 1, views: 120, published: true },
  ],
  authors: [{ id: 1, name: "Ada Lovelace" }],
});
 
export const dataProvider: DataProvider = {
  ...baseDataProvider,
  async getList(resource, params) {
    const { q, ...filter } = params.filter ?? {};
    if (resource !== "posts" || typeof q !== "string" || q === "") {
      return baseDataProvider.getList(resource, { ...params, filter });
    }
 
    const result = await baseDataProvider.getList(resource, {
      ...params,
      filter,
      pagination: undefined,
    });
    const search = q.toLowerCase();
    const data = result.data.filter((post) =>
      [post.title, post.body].some((value) =>
        String(value ?? "").toLowerCase().includes(search),
      ),
    );
    const pagination = params.pagination;
    const pageData = pagination && "page" in pagination
      ? data.slice(
          (pagination.page - 1) * pagination.perPage,
          pagination.page * pagination.perPage,
        )
      : data;
    return { data: pageData, total: data.length };
  },
};

Mount the admin

Wrap everything in a router and <Admin>, pass the resources registry, and add the opt-in shadcn shell:

src/App.tsx
import { Admin, Layout, Resource } from "@chelone/react";
import { QueryClient } from "@tanstack/react-query";
import { BrowserRouter } from "react-router";
import { dataProvider } from "./data-provider";
import { resources } from "./resources";
import { PostList } from "./posts";
 
const queryClient = new QueryClient();
 
export function App() {
  return (
    <BrowserRouter>
      <Admin
        dataProvider={dataProvider}
        queryClient={queryClient}
        resources={resources}
      >
        <Admin.Layout>
          <Layout />
        </Admin.Layout>
        <Resource name="posts" list={PostList} />
      </Admin>
    </BrowserRouter>
  );
}

Next: build the list.