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:
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>:
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:
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.