ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

とりあえずCRUDを確認できる画面を実装したい、react-adminで

前書き

ミツモアでエンジニアやってる もっさん(当然偽名)です! アドベントカレンダーの役回りをもらい、もう、ほんっとうに書くことがなくて 他の方の記事をちら見してると、やっぱりAI関連が多いわけですね。

で、最近よく「AI使って〇〇みたいなの作ってよ(適当)」と言われて、それがバックエンドのロジックメインの時、フロントエンドを実装するのが大変億劫です。 少なくとも現時点の生成AIの文脈だと「枠組み(前提)があるかないか」で成果物が大きく変わる認識で、億劫な フロントエンドの枠組みを提供してくれるやつに焦点を当てて、簡単な使い方とデモを書き連ねることで誤魔化したいと思います…。

この強引な導入で紹介するのは react-admin。 名前の通り、Reactで管理画面をサクッと作れる子で、MUIベースのフロントエンドフレームワークです。

カスタマイズしなくてもそれっぽく動きますし、作りが丁寧なので「こう変えたいんだけどなー」が大体できる、今回の趣旨にもぴったりなやつです(適当)。


真面目な 前書き

こういう系って「見ながら・触りながら」追えるものが意外と少ないんですよね。特にチュートリアル寄りのやつ。 「ドキュメント見ろ甘えてんじゃねぇ」は100%の正論なんですが、丁寧なチュートリアル形式で一通り触れる状態だと、吸収できる量が増えるな〜と思ったり思わなかったりです。

というわけで、以降は基本的な使い方を羅列しつつ、実際に触れるデモページも用意しました。適宜、触りながら確認してみてください!

個人的に借りてるサーバーなので無茶しないでね


この記事でやること

「AI使って〇〇みたいなの作ってよ。え?動くやつは見れるようにして(適当)」って言われたときに、まず最低限これが揃ってると助かるよね、を作ります。

  • dataProvider とは何か
  • 最短CRUD(List / Create / Edit / Show)
  • 保存後の遷移・通知を制御する
  • リレーションの最低限
    • many-to-one(投稿 → 著者)
    • one-to-many(投稿 → コメント)
    • one-to-one(投稿 → 詳細)
  • 一覧を「画面内スクロール」にして運用しやすくする
  • ra-data-simple-rest で足りない場合の custom dataProvider

ここでの「最短」は、“実装量が少ない”だけじゃなくて、“後から色々追加要求を受けても壊れにくい”も含みます。


0. まず dataProvider を押さえる

react-adminで最初に詰まりがちなのがここです。

  • Listコンポーネントを書いた
  • でも、データが…ない
  • というか、どこから…取るの?
  • axiosは?fetchは?どこに書くの?

結論:react-adminは、画面側で直接 fetch/axios を書かずに、dataProvider という“通信の窓口”を通します。 画面は「postsを一覧したい」「id=1を取得したい」みたいな“意図”だけ伝えて、HTTPの詳細はdataProviderが責任を持つ、という設計です(=責務分離)。

dataProvider の役割

  • react-adminからの要求(例:getList('posts', params))を受ける
  • API形式に合わせてHTTPリクエストを組み立てる(REST / GraphQL / RPC / SOAPでも)
  • レスポンスを、react-adminが期待する形({ data, total } 等)に変換して返す

これがあると何が嬉しいかというと、

  • UIが汚れない(「このAPIのfilterはこう」みたいな癖を画面に漏らさない)
  • API形式が変わっても直す場所が一箇所に寄る
  • “バックエンドがまだ”でも、モックdataProviderで先に画面が作れる

…という、地味に重要な運用メリットがあります。 生成AIがばーっと実装しても役割が明確なので、結果的に読みやすくなったりもします。


1. セットアップ

まずはサクッと動く状態を整えます。適当な作業ディレクトリで以下を実行してください。

npm install react-admin
npm create react-admin@latest my-admin
cd my-admin
npm install
npm run dev

ブラウザが開いてウェルカムページが出ていればOKです。

なおこの記事は Vite ベースの構成で話を進めますが、Next.js や Remix 等にも対応しています(詳細は公式ドキュメント参照)。


2. ra-data-simple-rest で一覧を出す

RESTで素直なAPIなら、まずは既製のdataProviderを使うのが最速です。 ra-data-simple-rest は、フィルタやソート等を「GETクエリ」で渡すタイプのRESTに向いてます。

2.1 dataProvider

// src/dataProvider.ts
import simpleRestProvider from "ra-data-simple-rest";

export const dataProvider = simpleRestProvider("<http://localhost:3000>");

2.2 API側のレスポンス形式

当然ですがreact-admin側が期待しているレスポンス形式があります。 割と普遍的な形なので、API側の実装も以下の形式に沿うように実装してあげるとdataProviderで形式を変更してあげる必要がなくなるので楽です。

// 例:posts の Record 型(最小)
// ※ Identifier は string | number 相当(react-admin側の型)
import type { Identifier, RaRecord } from "react-admin";

export type PostRecord = RaRecord & {
  title: string;
  body?: string;
  created_at: string; // 例: ISO文字列でもOK
  author_id?: Identifier;
};

具体的なサンプルは以下の通り。

getList('posts', ...) の戻り値(一覧)

  • data: レコード配列(各要素が id を持つ)
  • total: 全件数(ページネーションのために必要)
// dataProvider.getList の戻り値イメージ
{
  data: [
    { id: 1, title: "Hello", body: "...", created_at: "2025-12-15T00:00:00.000Z", author_id: 10 },
    { id: 2, title: "World", body: "...", created_at: "2025-12-14T00:00:00.000Z", author_id: 11 }
  ],
  total: 235
}

getOne('posts', { id }) の戻り値(詳細)

// dataProvider.getOne の戻り値イメージ
{
  data: { id: 1, title: "Hello", body: "....", created_at: "2025-12-15T00:00:00.000Z", author_id: 10 }
}

重要:id が無い/別名(post_id とか)になっている場合、UI側で頑張るのではなく dataProvider側で id にマッピングして返すのが基本です(=責務分離の恩恵が出るところ)。

2.3 Admin + Resource

// src/App.tsx
import { Admin, Resource } from "react-admin";
import { dataProvider } from "./dataProvider";
import { PostList } from "./posts/PostList";
import { PostCreate } from "./posts/PostCreate";
import { PostEdit } from "./posts/PostEdit";
import { PostShow } from "./posts/PostShow";

export const App = () => (
  <Admin dataProvider={dataProvider}>
    <Resource
      name="posts"
      list={PostList}
      create={PostCreate}
      edit={PostEdit}
      show={PostShow}
    />
  </Admin>
);

Resource name="posts" が「postsというリソースを扱う」宣言になり、以降は name="posts" に紐づくCRUD画面が組み上がっていきます。


3. CRUDを揃える(List / Create / Edit / Show)

3.1 List(一覧)

// src/posts/PostList.tsx
import { List, DataTable, DateField } from "react-admin";

export const PostList = () => (
  <List>
    <DataTable rowClick="edit" bulkActionButtons={false}>
      {/* sourceに記載する値は、レスポンスbodyの各keyを指してください */}
      <DataTable.Col source="id" />
      <DataTable.Col source="title" />
      <DataTable.Col source="created_at">
        <DateField source="created_at" showTime />
      </DataTable.Col>
    </DataTable>
  </List>
);

ここまでで「一覧が出る」=管理画面っぽさが一気に出ます。

3.2 Create(新規作成)

// src/posts/PostCreate.tsx
import { Create, SimpleForm, TextInput, required } from "react-admin";

export const PostCreate = () => (
  <Create>
    <SimpleForm>
      <TextInput source="title" validate={[required()]} fullWidth />
      <TextInput source="body" multiline fullWidth />
    </SimpleForm>
  </Create>
);

3.3 Edit(編集)

// src/posts/PostEdit.tsx
import { Edit, SimpleForm, TextInput, required } from "react-admin";

export const PostEdit = () => (
  <Edit>
    <SimpleForm>
      <TextInput source="id" disabled />
      <TextInput source="title" validate={[required()]} fullWidth />
      <TextInput source="body" multiline fullWidth />
    </SimpleForm>
  </Edit>
);

3.4 Show(詳細)

// src/posts/PostShow.tsx
import { Show, SimpleShowLayout, TextField } from "react-admin";

export const PostShow = () => (
  <Show>
    <SimpleShowLayout>
      <TextField source="id" />
      <TextField source="title" />
      <TextField source="body" />
    </SimpleShowLayout>
  </Show>
);

4. 保存後の遷移・通知

CRUDが揃うと、次に欲しくなるのが「作成・更新後の挙動の調整」です。 ユースケースはだいたいこの辺。

  • 保存したあと、自分自身の画面(list/show/edit)ではなく別画面に遷移させたい
  • 遷移タイミングを「保存成功後」にしたい(=レスポンスを受けてから動きたい)
  • 連続編集したいので、次のリソースに遷移させたい

react-adminはこの辺も用意されてて、成功時の挙動をコンポーネント側で宣言的に制御できます。

// src/posts/PostEdit.tsx
import { Edit, SimpleForm, TextInput, useNotify, useRedirect } from "react-admin";

export const PostEdit = () => {
  const notify = useNotify();
  const redirect = useRedirect();

  return (
    <Edit
      mutationOptions={{
        onSuccess: () => {
          notify("保存しました", { type: "info" });
          redirect("list", "posts");
        },
      }}
    >
      <SimpleForm>
        <TextInput source="id" disabled />
        <TextInput source="title" fullWidth />
        <TextInput source="body" multiline fullWidth />
      </SimpleForm>
    </Edit>
  );
};
  • notify() で「保存した」が即分かる
  • redirect() で「保存後は一覧へ戻る」「ページに止まる」など自由に設計できる

「一覧へ戻す/編集画面に留まる/詳細へ飛ぶ」など、運用の都合で正解が変わるところなので、こういう“後から変えられる余白”は偉いです。


5. フィールド・入力の見た目と構造(「それっぽい」画面にする小技)

ここからは、完全におまけです。 でも“それっぽさ”の差は意外とここで出ます。

5.1 sx で崩れないレイアウトを作る

フォームが縦にダラっと並ぶと、情報量が増えたときに視認性が落ちます。 MUIベースなので、sx でレイアウトを整理しやすいです。

// src/posts/PostEdit.tsx
import { Edit, SimpleForm, TextInput } from "react-admin";
import { Box } from "@mui/material";

export const PostEdit = () => (
  <Edit>
    <SimpleForm>
      <Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2 }}>
        <TextInput source="title" fullWidth />
        <TextInput source="slug" fullWidth />
      </Box>

      <TextInput source="body" multiline fullWidth sx={{ mt: 2 }} />
    </SimpleForm>
  </Edit>
);

5.2 独自Fieldを作る

「この列、文字列だけじゃなくてアイコン付けたい」「idとtitleをまとめたい」みたいなやつです。

// src/posts/fields/TitleWithId.tsx
import { useRecordContext } from "react-admin";

type TitleWithIdProps = {
  separator?: string;
};

type PostRecord = {
  id: string | number;
  title?: string;
};

export const TitleWithId = (props: TitleWithIdProps) => {
  const record = useRecordContext<PostRecord>();
  if (!record) return null;
  return (
    <span>
      {record.id}
      {props.separator ?? ": "}
      {record.title ?? ""}
    </span>
  );
};

使う側はこう。

import { List, DataTable } from "react-admin";
import { TitleWithId } from "./fields/TitleWithId";

export const PostList = () => (
  <List>
    <DataTable rowClick="edit" bulkActionButtons={false}>
      <DataTable.Col label="ID / Title">
        <TitleWithId separator=" / " />
      </DataTable.Col>
    </DataTable>
  </List>
);

「列=コンポーネント」になってくると、表現の自由度が急に上がります。

5.3 独自Inputを作る(useInput に寄せる)

react-adminのフォームはreact-hook-formで制御されます。 独自Inputを作るなら useInput に寄せておくと、バリデーションや送信の仕組みに自然に乗れます。

例として「入力を大文字に整形するInput」を置きます。

// src/common/inputs/UppercaseTextInput.tsx
import { TextField } from "@mui/material";
import { useInput } from "react-admin";

type UppercaseTextInputProps = {
  source: string;
  label?: string;
  fullWidth?: boolean;
};

export const UppercaseTextInput = (props: UppercaseTextInputProps) => {
  const { field, fieldState, id, isRequired } = useInput({
    source: props.source,
  });

  return (
    <TextField
      id={id}
      label={props.label}
      required={isRequired}
      fullWidth={props.fullWidth}
      value={typeof field.value === "string" ? field.value : ""}
      onChange={(e) => field.onChange(e.target.value.toUpperCase())}
      onBlur={field.onBlur}
      error={!!fieldState.error}
      helperText={fieldState.error?.message}
    />
  );
};

使う側:

import { Edit, SimpleForm } from "react-admin";
import { UppercaseTextInput } from "../common/inputs/UppercaseTextInput";

export const PostEdit = () => (
  <Edit>
    <SimpleForm>
      <UppercaseTextInput source="slug" label="Slug" fullWidth />
    </SimpleForm>
  </Edit>
);

6. リレーション:many-to-one / one-to-many / one-to-one(「関連する〇〇も出して」)

よくある要求ですね、作るのはめんどくさいぞー…と。

「投稿に著者名も出して」 「投稿のコメントも見たい」 「詳細情報(1対1)も編集したい」

react-adminはこの辺も“関係性コンポーネント”が用意されてます。 以降の例は、こういうデータを想定します。

  • posts: { id, title, body, author_id }
  • users: { id, name }
  • comments: { id, post_id, body }
  • post_details: { id, post_id, seo_title, seo_description }

6.1 many-to-one(Post → User):著者名を表示&選択

一覧で表示(ReferenceField)

import { List, DataTable, TextField, ReferenceField } from "react-admin";

export const PostList = () => (
  <List>
    <DataTable rowClick="edit" bulkActionButtons={false}>
      <DataTable.Col source="title" />
      <DataTable.Col label="Author">
        <ReferenceField source="author_id" reference="users" link="show">
          <TextField source="name" />
        </ReferenceField>
      </DataTable.Col>
    </DataTable>
  </List>
);

Editで選択(ReferenceInput + SelectInput)

import { Edit, SimpleForm, TextInput, ReferenceInput, SelectInput } from "react-admin";

export const PostEdit = () => (
  <Edit>
    <SimpleForm>
      <TextInput source="title" fullWidth />
      <ReferenceInput source="author_id" reference="users">
        <SelectInput optionText="name" fullWidth />
      </ReferenceInput>
    </SimpleForm>
  </Edit>
);

6.2 one-to-many(Post → Comments):Showにコメント一覧をぶら下げる

外部キーが子側(comments.post_id)にある典型パターンです。

import { Show, SimpleShowLayout, TextField, ReferenceManyField, DataTable } from "react-admin";

export const PostShow = () => (
  <Show>
    <SimpleShowLayout>
      <TextField source="id" />
      <TextField source="title" />

      <ReferenceManyField reference="comments" target="post_id" label="コメント">
        <DataTable bulkActionButtons={false}>
          <DataTable.Col source="id" />
          <DataTable.Col source="body" />
        </DataTable>
      </ReferenceManyField>
    </SimpleShowLayout>
  </Show>
);

裏では dataProvider.getManyReference が呼ばれます。つまり、リレーションも「画面ではなく dataProvider 側の責務」として閉じられるのが良いところです。

6.3 one-to-one(Post → PostDetails):詳細を表示する

1対1も、やりたいことは「1件取って表示」。 ReferenceOneFieldgetManyReference で取って先頭を使う、という挙動になります。

import { Show, SimpleShowLayout, TextField, ReferenceOneField } from "react-admin";

export const PostShow = () => (
  <Show>
    <SimpleShowLayout>
      <TextField source="title" />

      <ReferenceOneField reference="post_details" target="post_id" label="詳細">
        <SimpleShowLayout>
          <TextField source="seo_title" />
          <TextField source="seo_description" />
        </SimpleShowLayout>
      </ReferenceOneField>
    </SimpleShowLayout>
  </Show>
);

1対1の“編集”について(注意)

ReferenceOneInput のような「1対1編集」コンポーネントもありますが、Enterprise版のコンポーネントとして提供されます。 本記事では「まず表示できる」までに留めます。


7. 一覧を「画面内スクロール」にする

CRUD/リレーションが一通り揃うと、次に運用で地味に効いてくるのが一覧の使い勝手です。 一般的な管理画面だと「ページ全体」ではなく テーブル部分だけスクロールする構成が多いと思います。

この章は Layout 側も少し触るので、必要になったタイミングで読むのがおすすめです。

特に避けたいのはこの辺。

  • 一覧が縦に伸びすぎて、検索やアクションがスクロールの彼方へ
  • 画面上部に戻るのがだるい
  • ヘッダー(列名)も見失う

react-adminの各種コンポーネントは、既存の仕組みに乗っかりつつ外からレイアウト調整できるので、「仕組みはそのまま・見た目だけ整える」がやりやすいです。

ここでは List を分解して ListBase + ListView で組み直し、ツールバーとページネーションを“スクロールの外”に出します。

前提:Layout 側で「高さ」と「スクロール」を決める

List 側で height: 100% を使うなら、親(コンテンツ領域)の高さが確定している必要があります。 ページ全体(body)が縦に伸びてスクロールしてしまうと、結局「ページ全体スクロール」になってしまうので、先に Layout 側で「スクロールさせる箱」を決めておくのがコツです。

// src/layout/CustomLayout.tsx(デモの考え方)
import { AppBar, Sidebar, Menu } from "react-admin";
import { Box, useTheme } from "@mui/material";

type CustomLayoutProps = { children: React.ReactNode };

export const CustomLayout = (props: CustomLayoutProps) => {
  const theme = useTheme();

  return (
    <Box sx={{ display: "flex", height: "100vh" }}>
      <Box sx={{ height: "100%", zIndex: theme.zIndex.appBar - 1 }}>
        <Sidebar>
          <Menu />
        </Sidebar>
      </Box>

      <AppBar alwaysOn={false} />

      <Box
        component="main"
        sx={{
          height: "100vh",
          width: "calc(100% - 50px)",
          display: "flex",
          flexDirection: "column",
        }}
      >
        <Box id="main-content" sx={{ flex: 1, overflow: "auto" }}>
          {props.children}
        </Box>
      </Box>
    </Box>
  );
};

これは Adminlayout prop(例:<Admin layout={CustomLayout} ...>)で差し込めます。

実アプリでは Layout の .RaLayout-content に height/overflow を当てる形でもOKです。重要なのは「スクロールさせる箱を1つに決める」ことです。

// src/posts/PostList.tsx
import {
  DataTable,
  DateField,
  ShowButton,
  EditButton,
  ListViewProps,
  ListBase,
  ListView,
  ListToolbar,
} from "react-admin";
import { Box, Paper } from "@mui/material";

const dataTableStyles = {
  // テーブル部分だけスクロールさせる(ページ全体は伸ばさない)
  "& .RaDataTable-tableWrapper": {
    maxHeight: "calc(100vh - 120px)",
    overflow: "auto",
  },
  // 横が溢れやすいので、列は折り返さずに見せる(必要なら ellipsis 等を追加)
  "& .RaDataTable-headerCell, & .RaDataTable-rowCell": {
    whiteSpace: "nowrap",
    textOverflow: "ellipsis",
  },
};

const PostDataTable = () => {
  return (
    <DataTable rowClick="show" bulkActionButtons={false} sx={{ ...dataTableStyles }}>
      <DataTable.Col source="id" />
      <DataTable.Col source="title" />
      <DataTable.Col source="status" />
      <DataTable.Col source="author" />
      <DataTable.Col source="created_at">
        <DateField source="created_at" showTime />
      </DataTable.Col>
      <DataTable.Col>
        <ShowButton />
        <EditButton />
      </DataTable.Col>
    </DataTable>
  );
};

export const PostList = (props: ListViewProps) => {
  const { pagination, filters, actions } = props;

  return (
    <ListBase perPage={25} sort={{ field: "id", order: "ASC" }}>
      <ListView>
        <Box sx={{ display: "flex", flexDirection: "column", height: "100%", minHeight: 0, position: "relative" }}>
          <ListToolbar filters={filters} actions={actions} />
          <Paper sx={{ flex: 1, minHeight: 0, overflow: "hidden", position: "relative" }}>
            <PostDataTable />
          </Paper>
          <Box sx={{ position: "absolute", bottom: 0, right: 16 }}>{pagination}</Box>
        </Box>
      </ListView>
    </ListBase>
  );
};

ポイントはこんな感じです。

  • 前提:Layout 側でコンテンツ領域の高さを固定し、スクロールさせる箱を1つに決める(デモは #main-content
  • .RaDataTable-tableWrapper を狙って“テーブルだけスクロール”に寄せる
  • 親は height: 100% + minHeight: 0(flex + overflow の定番罠回避)
  • ツールバー/ページネーションをスクロール外に出して「操作がスクロールの彼方へ」を防ぐ
  • DataTable は列ヘッダがデフォルトで sticky(列名を見失いにくい)

calc(100vh - 120px) の 120px は「あなたのヘッダー/フィルタ/ページネーションの高さ」に合わせて調整してください。ここだけはUIと相談です(でも調整箇所はここ1箇所で済む)。 わかりやすさのため、react-adminの を大胆に書き換えていますが、継承する形でも問題はないです。


8. ra-data-simple-rest で足りない場合の dataProvider 定義(axios版)

ra-data-simple-rest が合わないケースもあります。たとえば、

  • クエリ形式が独自(filter/sort/rangeの形が違う)
  • 送信時にデータを付与しないといけない
  • レスポンスの形が { items, total } みたいに違う
  • パスが /api/v1/... で、リソースごとに歪みがある
  • 認証ヘッダやリトライ、共通エラー整形を入れたい

こういう時に、画面に通信の実装を漏らさず、dataProvider に閉じ込めるのが正解です。 ここでは axios でAPIを叩く custom dataProvider の例を置きます(デモ実装寄り)。

// src/api.ts
import axios from "axios";

export const api = axios.create({
  baseURL: "<http://localhost:3000>",
  // 認証が cookie ベースなら withCredentials: true,
});
// src/dataProvider.ts
import type { DataProvider } from "react-admin";
import { api } from "./api";

type ListResponse<T> = {
  items: T[];
  total: number;
};

export const dataProvider: DataProvider = {
  getList: async (resource, params) => {
    const page = params.pagination?.page ?? 1;
    const perPage = params.pagination?.perPage ?? 10;
    const field = params.sort?.field ?? "id";
    const order = params.sort?.order ?? "ASC";

    const res = await api.get<ListResponse<unknown>>(`/${resource}`, {
      params: {
        page,
        perPage,
        sort: field,
        order,
        filter: params.filter,
      },
    });

    return { data: res.data.items as unknown as { id: string | number }[], total: res.data.total };
  },

  getOne: async (resource, params) => {
    const res = await api.get<unknown>(`/${resource}/${params.id}`);
    return { data: res.data as unknown as { id: string | number } };
  },

  getMany: async (resource, params) => {
    const res = await api.get<ListResponse<unknown>>(`/${resource}`, {
      params: {
        page: 1,
        perPage: params.ids.length,
        sort: "id",
        order: "ASC",
        filter: { id: params.ids },
      },
    });

    return { data: res.data.items as unknown as { id: string | number }[] };
  },

  getManyReference: async (resource, params) => {
    const page = params.pagination?.page ?? 1;
    const perPage = params.pagination?.perPage ?? 10;
    const field = params.sort?.field ?? "id";
    const order = params.sort?.order ?? "ASC";

    const res = await api.get<ListResponse<unknown>>(`/${resource}`, {
      params: {
        page,
        perPage,
        sort: field,
        order,
        filter: {
          ...(params.filter ?? {}),
          [params.target]: params.id,
        },
      },
    });

    return { data: res.data.items as unknown as { id: string | number }[], total: res.data.total };
  },

  update: async (resource, params) => {
    const res = await api.put<unknown>(`/${resource}/${params.id}`, params.data);
    return { data: res.data as unknown as { id: string | number } };
  },

  updateMany: async (resource, params) => {
    await Promise.all(params.ids.map((id) => api.put(`/${resource}/${id}`, params.data)));
    return { data: params.ids };
  },

  create: async (resource, params) => {
    const res = await api.post<unknown>(`/${resource}`, params.data);
    return { data: res.data as unknown as { id: string | number } };
  },

  delete: async (resource, params) => {
    const res = await api.delete<unknown>(`/${resource}/${params.id}`);
    return { data: res.data as unknown as { id: string | number } };
  },

  deleteMany: async (resource, params) => {
    await Promise.all(params.ids.map((id) => api.delete(`/${resource}/${id}`)));
    return { data: params.ids };
  },
};

9. バックエンド側に求められる最低限

この記事のコードは「そのままでは動かないが、周辺を用意してコピペすれば動く」前提だったりするので、最低限の契約を書いておきます。

CRUDの最低限(posts)

  • GET /posts(ページング/ソート/フィルタを受ける)
  • GET /posts/:id
  • POST /posts
  • PUT /posts/:id
  • DELETE /posts/:id

リレーション用(例)

  • GET /users / GET /users/:id
  • GET /comments?filter[post_id]=...(または同等)
  • GET /post_details?filter[post_id]=...(または同等)

ra-data-simple-rest の方言に乗るなら、その形式に合わせれば繋がります。独自方言ならcustom dataProviderで吸収します。


おまけ:バックエンドがまだでも画面を作りたい(モック dataProvider

「バックエンドは後で」って言われがちな現場向けに、クライアント側だけでデータを生やす ra-data-fakerest という選択肢もあります。 まず画面を固めて、後からAPI接続に置き換える、ができます。


おわりに

つらつらと記載しましたが、今回はかなり最低限のできることに留めています。 公式ドキュメントを見てもらうと項目が多いのと、7章で紹介した画面内スクロールみたいに「ドキュメントに載ってないけど便利」な部品が、びっくりするくらい紛れてたりもします。

(ドキュメントに羅列だけでもいいからして欲しい…切実に)

大体のことはできるし、拡張性もかなり高いので、ぜひ快適なフロントエンド作成につなげてもらえるとです。 ここまで枠組みを用意してあげれば、AIエージェントに「〇〇のCRUD作って。リクエストとレスポンスはこんな感じ(型定義添付)」で、大体それっぽいものを出してくれます。

今回はreact-adminを取り上げましたが、Refineでも似たことができるので、機会があればそっちの紹介と比較もできれば。

ではでは〜。

そういえばの蛇足

そういえば、エンジニア募集しているのでよければ見てやってください。 採用情報:https://hrmos.co/pages/meetsmore/jobs

エンジニアだったら私が採用フローに出没します...。


参考