昨今のAI時代、AIがどんどんコードを書いてくれます。AIに適切な指示を与えられればAIが大量のコードを高速で書いてくれるので、良いフレームワークを探したりしなくてもいい時代になっているように思えます。さらにAIは流行っているフレームワークを使ってくれる事が多いと思います。
ただし、小さなスコープのコードなら雑な指示(プロンプト)でも望むコードができる場合が多いですが、大きなプログラム(サービス全体)の場合は仕様駆動開発(Spec-Driven Development)などの整然とした指示書を書く必要があります。それでも思っていないコードが書かれてしまう事もあり疲れます。😅
今回のブログはCedarJSというフルスタック・フレームワークの入門記事です。久しぶりに高機能フルスタック・フレームワークを使ってみると、こういうのも良いなと思いました。😄
Bing Image Creatorが生成した画像を使っています
2025年のフルスタック・フレームワーク
React Server Components(RSC)がリリースされてから随分と時間が経ちましたね(Next.jsでは2023年、Reactでは2024年に正式リリースされました)。RSCは従来のブラウザー(フロントエンド)でのみ動くライブラリーからサーバー(バックエンド)でも動くライブラリーへと進化しました。
RSCはバックエンドとして利用するのではなくBackends For Frontends(BFF)として使うのが望ましいと言われていますが、現在RSCの実行環境としての人気のNext.jsはフロントエンドとバックエンドを含むフルスタック・フレームワークとしても使えます。
また、RSCが登場したことで以前から存在したフルスタック・フレームワークに大きな影響を与えました。
Remixは次期Remix3でReactとは決別すると発表され驚きました(参照:Remix 3 発表まとめ)。
さらに、以前このブログでも取り上げたRedwoodJSは開発を停止してしまいました。ただし、RedwoodJSはCloudflare用開発環境RedwoodSDKに生まれ変わり、RedwoodJSから派生したCedarJSの開発は活発なようです。
CedarJSとは
AI(perplexity)の要約
CedarJSは、Reactフロントエンド、GraphQL API、Prismaデータベース、認証、テスト、デプロイ機能をすべて標準搭載した、意見重視型のフルスタックReactフレームワークです。RedwoodJS GraphQLフレームワークをフォークしたもので、今も活発に開発されており、企業や個人/スタートアップが素早く本格的なWebアプリを構築するために使われています。
主な特徴
- オールインワン:React、GraphQL、Prismaなど最新の技術が統合されているため、複雑な設定やツールの管理を気にせず開発に集中できます。
- 開発生産性と体験重視:標準化された構成と明確な規約で、開発者やチームが統一されたやり方で迅速に開発可能です。
- RedwoodJS経験者向け:RedwoodJSを使っていたプロジェクトからの移行や、新たに積極的な開発が期待できるフレームワークを望むユーザーに適しています。
利用シーン
- MVPや業務ツール、本格的なサービスまで幅広く対応でき、ソロ開発者から企業チームまで幅広く利用可能です。
RedwoodJSからの進化
- RedwoodJSの設計思想を引き継ぎつつ、より現代的なWeb標準と開発者体験にフォーカスした新機能や改善が加えられています。
このように、CedarJSは「Webアプリ開発に必要なものがすべて揃い、すぐに使える」ことを重視し、開発効率やプロダクト品質の向上をサポートするフレームワークです。
補足
私が感じた良い点で、AI要約には無いものを書いておきます。
- Vitestを内蔵、コードジェネレーターがBackendのテストコードの一部を生成してくれます
- StorybookもサポートしていてReactコンポーネント単位での開発・テストができます
- Cellsという通信を抽象化してくれる機構を持っています
- ReactのSuspense + 通信機能のようなものです
- Reactの開発環境はViteなのでViteの知識やプラグインが使えます
- Ruby on Railsのように豊富な開発コマンドやコードジェネレーターをもっています
いつものジャンケンアプリをCedarJSで書いてみました

インストール
yarnコマンドを使う事を前提としているようです、最初npmを使ったのですがエラーが出たので止めました。
- ① プロジェクトを作ってくれるcreateコマンドがサポートされています
- インストール中にエラーが発生した時に、続ける/中止 の確認があります
- また、git initするかの確認もあります
- ② Tailwind CSSの利用は専用コマンドできます(redwoodから引き継がれたコマンドですね)
- Tailwind CSS以外のChakra UI、Mantine UIが選択できるようです
$ yarn create cedar-app jyanken --typescript ← ①
$ cd jyanken
$ yarn install
$ yarn redwood setup ui tailwindcss ← ②
データベース作成
データベース操作にはPrismaを使っているので、テーブル定義はPrismaのスキーマ定義を使います
api/db/schema.prisma
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model Score {
id Int @id @default(autoincrement())
human Int
computer Int
judgment Int
matchDate DateTime @default(now())
}
コードで生成
- ① RDBのテーブル作成、Prismaのマイグレーションが行われます
- ② 指定したテーブルに対しての一覧表示・詳細表示・新規作成・変更・削除できる、GraphQLサーバーのコード、Reactコードを生成してくれます
- Railsと違いテーブル定義はPrismaから取得してくれます
- ③ 開発サーバーの起動、これはcedarコマンドですがredwood(rw)コマンドと同じようです
- GraphQLサーバーのGraphQL YogaやReact開発環境のViteやコードの変更を監視するツール類が起動されます
$ yarn redwood prisma migrate dev ← ①
$ yarn redwood generate scaffold score ← ②
$ yarn cedar dev ← ③
- 生成された画面の一部
- 昔のRailsのような画面が表示されます、ここでは今回使う一覧と新規作成の画面のみ取り上げました
matchDate DateTime @default(now())の定義のせいか、新規作成の画面でデータを作成するとエラーになってしまいますが気にぜず進めます


バックエンドの変更
api/src/services/scores/scores.ts
バックエンドのサービスは一覧表示・詳細表示・新規作成・変更・削除のコードでしたが、ジャンケンアプリ用に変更しました
- ① 一覧のソートと件数の制限を追加
- ② ジャンケンの実行コードを追加
import type { QueryResolvers, MutationResolvers } from 'types/graphql'
import { db } from 'src/lib/db'
export const scores: QueryResolvers['scores'] = () => {
// return db.score.findMany() ← scaffoldの生成した元コード
return db.score.findMany({ orderBy: { id: 'desc' }, take: 15 }) // ← ①
}
export const score: QueryResolvers['score'] = ({ id }) => {
return db.score.findUnique({
where: { id },
})
}
export const createScore: MutationResolvers['createScore'] = ({ input }) => {
const human = input.human // ← ②
const computer = Math.floor(Math.random() * 3) // ← ②
const judgment = (computer - human + 3) % 3 // ← ②
return db.score.create({
// data: input, ← scaffoldの生成した元コード
data: { human, computer, judgment }, // ← ②
})
}
export const updateScore: MutationResolvers['updateScore'] = ({
id,
input,
}) => {
return db.score.update({
data: input,
where: { id },
})
}
export const deleteScore: MutationResolvers['deleteScore'] = ({ id }) => {
return db.score.delete({
where: { id },
})
}
api/src/graphql/scores.sdl.ts
GraphQLの定義ファイルも変更しました
- ① 新規作成(CreateScoreInput)では、人間の手humanのみ送られるように不要な項目を削除さしました
export const schema = gql`
type Score {
id: Int!
human: Int!
computer: Int!
judgment: Int!
matchDate: DateTime!
}
type Query {
scores: [Score!]! @requireAuth
score(id: Int!): Score @requireAuth
}
input CreateScoreInput {
human: Int! # ↓ ①
# computer: Int! ← scaffoldの生成した元コード
# judgment: Int! ← scaffoldの生成した元コード
# matchDate: DateTime! ← scaffoldの生成した元コード
}
input UpdateScoreInput {
human: Int
computer: Int
judgment: Int
matchDate: DateTime
}
type Mutation {
createScore(input: CreateScoreInput!): Score! @requireAuth
updateScore(id: Int!, input: UpdateScoreInput!): Score! @requireAuth
deleteScore(id: Int!): Score! @requireAuth
}
`
再起動とUI変更
scores.sdl.tsの変更がReact(UI)側に反映されなかったので、再起動しました。何か良いコマンドがあるのかな?
^C
$ yarn cedar dev
新規作成画面からcomputer、judgment入力欄を削除し、humanに人間の手を入力するとジャンケンが出来るようになりました。

フロントエンドの変更
フロントエンド(React)のコードを変更しジャンケンアプリらしくしました。
web/src/pages/Score/ScoresPage/ScoresPage.tsx
ジャンケンのトップページ
- ① もともと一覧表示ページでしたが新規作成(ジャンケンボタン)コンポーネントを追加しました。
import NewScore from 'src/components/Score/NewScore'
import ScoresCell from 'src/components/Score/ScoresCell'
const ScoresPage = () => {
return (
<>
<NewScore /> {/* ← ① */}
<ScoresCell />
</>
)
}
export default ScoresPage
web/src/components/Score/Scores/Scores.tsx
ジャンケン結果の一覧ページです。Scaffoldの作ったコードは、ほぼ書き変えました。
import type { FindScores } from 'types/graphql'
const ScoresList = ({ scores }: FindScores) => {
const teString = ['グー', 'チョキ', 'パー']
const judgmentString = ['引き分け', '勝ち', '負け']
const judgmentColors = ['text-[#000]', 'text-[#2979ff]', 'text-[#ff1744]']
const dateHHMMSS = (d: string) => new Date(d).toTimeString().substring(0, 8)
return (
<table className="w-full text-left text-sm text-gray-500">
<thead className="border bg-slate-100">
<tr>
<th className="px-6 py-3">時間</th>
<th className="px-6 py-3">人間</th>
<th className="px-6 py-3">コンピューター</th>
<th className="px-6 py-3">勝敗</th>
</tr>
</thead>
<tbody className="border bg-white">
{scores.map((score) => (
<tr
key={score.id}
className={`border-b bg-white ${judgmentColors[score.judgment]}`}
>
<td className="px-2 py-4 md:px-6">{dateHHMMSS(score.matchDate)}</td>
<td className="px-2 py-4 md:px-6">{teString[score.human]}</td>
<td className="px-2 py-4 md:px-6">{teString[score.computer]}</td>
<td className="px-2 py-4 md:px-6">
{judgmentString[score.judgment]}
</td>
</tr>
))}
</tbody>
</table>
)
}
export default ScoresList
web/src/components/Score/ScoresCell/ScoresCell.tsx
ジャンケン結果を受け取るCellsは、データが0件の場合の表示を無しにしました(export const Empty = () => {}を削除)。
実際の通信を行うコードはフレームワーク内にあり、以下のようにQUERY用GraphQL定義、ロード中、エラー、正常のコードを書くだけですみます。
import type { FindScores, FindScoresVariables } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps, TypedDocumentNode } from '@cedarjs/web'
import Scores from 'src/components/Score/Scores'
export const QUERY: TypedDocumentNode<FindScores, FindScoresVariables> = gql`
query FindScores {
scores {
id
human
computer
judgment
matchDate
}
}
`
export const Loading = () => <div>Loading...</div>
export const Failure = ({ error }: CellFailureProps<FindScores>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({ scores }: CellSuccessProps<FindScores, FindScoresVariables>) => {
return <Scores scores={scores} />
}
web/src/components/Score/NewScore/NewScore.tsx
新規作成コンポーネントはジャンケンのボタンの並ぶコンポーネントになります。
- ① useMutationでGraphQL送信の定義
- ② 通信完了後の再表示を設定しています
- ③ 通信の実行
import type {CreateScoreMutation, CreateScoreMutationVariables} from 'types/graphql'
import { useMutation } from '@cedarjs/web'
import type { TypedDocumentNode } from '@cedarjs/web'
import { toast } from '@cedarjs/web/toast'
import { QUERY } from '../ScoresCell'
const CREATE_SCORE_MUTATION: TypedDocumentNode<
CreateScoreMutation,
CreateScoreMutationVariables
> = gql`
mutation CreateScoreMutation($input: CreateScoreInput!) {
createScore(input: $input) {
id
}
}
`
const NewScore = () => {
const [createScore] = useMutation(CREATE_SCORE_MUTATION, { // ← ①
onCompleted: () => {},
onError: (error) => {
toast.error(error.message)
},
refetchQueries: [{ query: QUERY }], // ← ②
awaitRefetchQueries: true, // ← ②
})
const onSave = (te: number) => {
createScore({ variables: { input: { human: te } } }) // ← ③
}
const buttonClass = `text-white text-center text-sm rounded w-16 px-2 py-2
bg-blue-600 hover:bg-blue-700 shadow shadow-gray-800/50`
return (
<div className="w-[230px]mx-auto my-10 flex justify-center">
<button className={buttonClass} onClick={() => onSave(0)}>
グー
</button>
<button className={`${buttonClass} mx-5`} onClick={() => onSave(1)}>
チョキ
</button>
<button className={buttonClass} onClick={() => onSave(2)}>
パー
</button>
</div>
)
}
export default NewScore
web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
レイアウトファイルです。このファイルもジャンケンアプリ用に書きかえました。
import { Toaster } from '@cedarjs/web/toast'
type LayoutProps = {
children: React.ReactNode
}
const ScaffoldLayout = ({ children }: LayoutProps) => {
return (
<div>
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<header className="border-gray-50 bg-blue-600">
<div className="mx-auto flex max-w-screen-xl flex-wrap items-center p-3">
<h1 className="ml-5 text-2xl font-bold text-white">
じゃんけん ポン{' '}
</h1>
</div>
</header>
<main className="mx-2 md:mx-8 md:w-1/2">{children}</main>
</div>
)
}
export default ScaffoldLayout
web/src/Routes.tsx
CedarJSには昔のReact Routerのようなクラシカルな独自ルーティング機能を持っています。
- ① 今回のアプリでは
/scoresのみ使っています - ② 不要なルーティングは削除しましたが、ここではコメントで表示しておきます
- ③ ルート
/へのアクセスはscoresへリダイレクトするように指定しています
import { Set, Router, Route } from '@cedarjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
const Routes = () => {
return (
<Router>
<Set wrap={ScaffoldLayout}>
<Route path="/scores" page={ScoreScoresPage} name="scores" /> {/* ← ① */}
{/* ↓ ②
<Route path="/scores/new" page={ScoreNewScorePage} name="newScore" />
<Route path="/scores/{id:Int}/edit" page={ScoreEditScorePage} name="editScore" />
<Route path="/scores/{id:Int}" page={ScoreScorePage} name="score" />
*/}
</Set>
<Route path="/" redirect="scores" /> {/* ← ③ */}
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes
まとめ
大昔、Ruby on Railsが現れた時には、それまでのフレームワークとは違い使いやすく、さらに生産性の高さに驚かされました。今回説明したCedarJSにもRuby on Railsの影響が多くみられますね。
現在のClaude Code等のAIツールのコード生成能力の高さには驚かされます。少し(?)のプロンプトで面倒なコードを高速に大量に書いてくれます。しかし、飼い慣らすにはそれなりの試行錯誤や努力が必要です。
それに対してCedarJSのような高機能なフルスタック・フレームワークは自分たちが作りたいシステムと方向性があっていれば、ドキュメントやサンプルコードを読むけで高速にアプリが構築できるのが強みですね。
ただし、この大きなフレームワークの開発がずっと継続されるのかには少し不安も感じますね。そもそもCedarJSの元になったRedwoodJSの開発は停止してしているわけですから。
しかし、救いはGraphQL、Reactなど標準の技術をベースにしていることです、またViteやPrismaのような広く利用されている技術も多く利用されています。今回の説明で出てきたCells、ルーティングなどのCedarJS独自技術もありますが、これくらいなら自分達でもメンテ出来そうですね。
CedarJSというWebアプリケーション構築のベストプラクティスの詰まったフルスタック・フレームワークの今後に期待しています。










