EY-Office ブログ

AIのプロンプトに疲れたら、高機能フルスタック・フレームワークを使ってみては? CedarJS入門

昨今のAI時代、AIがどんどんコードを書いてくれます。AIに適切な指示を与えられればAIが大量のコードを高速で書いてくれるので、良いフレームワークを探したりしなくてもいい時代になっているように思えます。さらにAIは流行っているフレームワークを使ってくれる事が多いと思います。

ただし、小さなスコープのコードなら雑な指示(プロンプト)でも望むコードができる場合が多いですが、大きなプログラム(サービス全体)の場合は仕様駆動開発(Spec-Driven Development)などの整然とした指示書を書く必要があります。それでも思っていないコードが書かれてしまう事もあり疲れます。😅

今回のブログはCedarJSというフルスタック・フレームワークの入門記事です。久しぶりに高機能フルスタック・フレームワークを使ってみると、こういうのも良いなと思いました。😄

AI fatigue 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アプリケーション構築のベストプラクティスの詰まったフルスタック・フレームワークの今後に期待しています。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ