🔥

「とりあえずNext.js」をやめてBetter-T-Stackを試してみた

に公開

株式会社アップルワールドのrikuです。
※この記事はアップルワールド Advent Calendar 2025の9日目の記事です。

はじめに

2025年12月3日に起きたNext.jsおよびRSCのセキュリティ問題を受けて、弊社のフロントエンド技術選定について真剣に考える必要が出てきました。
今回の問題については各所で再三触れられているため、ここでは詳細を割愛します。

弊社では今まで利用者数が多いこと、AIによる開発と親和性が高いことを理由に2025年は「とりあえずNext.js」にすることが多かったのですが、弊社の規模・状況だと万が一問題が発生した際に会社存続のリスクに繋がる可能性があるので、2026年は並行して別の選択肢も試したいと考えました。

現在使用されているライブラリ・フレームワーク

私の知る限り、弊社のJavaScript/TypeScriptを利用した開発には主に以下のライブラリ・フレームワークが利用されています。

  • Next.js(AppRouter)
  • Vue2 + Vue Router
  • Vue3 + Vue Router
  • Nuxt3
  • Blitz.js
  • Refine(React)
  • (Nest.js)
  • (Hono)
  • (Express)
    ※カッコ書きはBFFとして利用

これまでのアップルワールドはプロジェクトごとに外部の開発会社へ完全委託したり、業務委託(フリーランス)を集めて開発したりと開発プロセスや技術選定の方針がプロジェクトメンバーに依存していたため、バラバラでした。また、M&Aによって事業譲渡を受けたものも複数あり、社内で各プロジェクトのナレッジを共有するハードルが高い状況が続いており、何か1つ軸となる技術選定の基準を欲しています。

2026年以降はNext.js以外の選択肢が欲しい

冒頭にも書いた通り、2025年はNext.js(+Hono)を利用することが多かったのですが、以下の懸念がありました。

  • メタフレームワークへの依存は今後の課題になりそう
  • AIを利用した開発生産性を最大化するためにmonorepo構成も検討したほうがよさそう

ということで、今回は上記をサクッと叶えてくれそうなBetter-T-Stackを使って、アプリケーションを構築してみようと思います。

Better-T-StackでWebアプリケーションを作ってみる

今回は以下の構成でWebアプリケーションを作成してみます。
自分でゼロから作ると少し大変そうですが、Better-T-Stackを使うとかなり楽に構築できます。最高ですね。

  • React
  • TanStack Start
  • Elysiajs
  • PostgreSQL
  • Drizzle
  • Better-Auth
  • Polar.sh
  • oRPC
  • Bun
  • Oxlint
  • Turborepo
  • Supabase

対話形式で環境構築する

さっそく始めましょう。基本的にCLI上でポチポチ選択するだけですが、OpenAPIとの親和性が高いと噂のElysiajsを利用したいので、先にBunを入れておきましょう。

Bunのinstall

curl -fsSL https://bun.sh/install | bash

Better-T-StackのCLIから構築

Better−T-Stackのドキュメントを読んでコマンドを実行しましょう。今回はBunを利用しますが、npmでもpnpmでも大丈夫みたいです。(UIを見る限りBun推奨のようですね)

bun create better-t-stack@latest

コマンドを実行すると以下のような画面になります。好きなものをポチポチと選択していきましょう。

私は最終的に以下のような構成にしました。今回は特に目的を定めないアプリケーション作成ですが、Better-Auth, Polar.sh, Turborepo, Oxlintあたりは触ったことがなかったので学習目的としてついでに入れておきました(この記事では詳細について触れません)。

今回の主な目的はTanStack StartとElysiajs、oRPCを使用することです。

成果物の確認

Better−T−Stackは以下の構成でアプリケーションを生成していました。

my-better-t-app/
├── README.md
├── apps
│   ├── server
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   └── web
│       ├── components.json
│       ├── package.json
│       ├── public
│       │   └── robots.txt
│       ├── src
│       │   ├── components
│       │   │   ├── header.tsx
│       │   │   ├── loader.tsx
│       │   │   ├── response.tsx
│       │   │   ├── sign-in-form.tsx
│       │   │   ├── sign-up-form.tsx
│       │   │   ├── ui
│       │   │   └── user-menu.tsx
│       │   ├── functions
│       │   │   ├── get-payment.ts
│       │   │   └── get-user.ts
│       │   ├── index.css
│       │   ├── lib
│       │   │   ├── auth-client.ts
│       │   │   └── utils.ts
│       │   ├── middleware
│       │   │   └── auth.ts
│       │   ├── router.tsx
│       │   ├── routes
│       │   │   ├── __root.tsx
│       │   │   ├── ai.tsx
│       │   │   ├── dashboard.tsx
│       │   │   ├── index.tsx
│       │   │   ├── login.tsx
│       │   │   ├── success.tsx
│       │   │   └── todos.tsx
│       │   └── utils
│       │       └── orpc.ts
│       ├── tsconfig.json
│       └── vite.config.ts
├── bts.jsonc
├── bun.lock
├── bunfig.toml
├── package.json
├── packages
│   ├── api
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── context.ts
│   │   │   ├── index.ts
│   │   │   └── routers
│   │   │       ├── index.ts
│   │   │       └── todo.ts
│   │   └── tsconfig.json
│   ├── auth
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   └── lib
│   │   │       └── payments.ts
│   │   └── tsconfig.json
│   ├── config
│   │   ├── package.json
│   │   └── tsconfig.base.json
│   └── db
│       ├── drizzle.config.ts
│       ├── package.json
│       ├── src
│       │   ├── index.ts
│       │   └── schema
│       │       ├── auth.ts
│       │       └── todo.ts
│       ├── supabase
│       │   └── config.toml
│       └── tsconfig.json
└── turbo.json
README.md
※一部抜粋

## Features

- **TypeScript** - For type safety and improved developer experience
- **TanStack Start** - SSR framework with TanStack Router
- **TailwindCSS** - Utility-first CSS for rapid UI development
- **shadcn/ui** - Reusable UI components
- **Elysia** - Type-safe, high-performance framework
- **oRPC** - End-to-end type-safe APIs with OpenAPI integration
- **Bun** - Runtime environment
- **Drizzle** - TypeScript-first ORM
- **PostgreSQL** - Database engine
- **Authentication** - Better-Auth
- **Turborepo** - Optimized monorepo build system

## Project Structure
my-better-t-app/
├── apps/
│   ├── web/         # Frontend application (React + TanStack Start)
│   └── server/      # Backend API (Elysia, ORPC)
├── packages/
│   ├── api/         # API layer / business logic
│   ├── auth/        # Authentication configuration & logic
│   └── db/          # Database schema & queries

素晴らしいですね。
サクッとアプリを作るならもうこれだけで十分でしょう。
もちろんTurborepoを含め全体の構成をちゃんと理解する必要はありますが、かなりの工数短縮に繋がることは間違いないと思います。

※もちろん、ここでNext.jsを選択することもできます。

開発環境を起動する

ではさっそく起動して試してみます。
最近弊社ではpnpmを利用することが増えてきたのですが、npm, yarn, pnpm, bunとコマンドを切り替えるのが大変になってきたのでniを使います。

ドキュメントの通りですが、下記いずれかのコマンドを実行することでniをinstallできます。

npm i -g @antfu/ni
brew install ni

niを使うと各種runコマンドはnr xxxで実行できます
例えば以下のように実行することで、使用されているパッケージマネージャーを自動で判別し、適切なscriptsを実行してくれます。

nr dev
# npm run dev
# yarn run dev
# pnpm run dev
# bun run dev
# deno task dev

これだけで開発環境が動きます。素晴らしいですね。


turbo devを利用しているのでターミナルには以下のような画面が表示されました。

http://localhost:3001/にアクセスすると、ブラウザでは以下のように表示されます。優秀なデバッガが最初から内包されているのがとても良いですね。開発体験を重視しているのがよくわかります。

余談: ni導入のきっかけ

niについてはcatnoseさんが利用しているのを見て導入を決定しました。npmとかpnpmなんていちいち気にしたくないですよね。私もniがないと生きていけないかもしれない。
https://x.com/catnose99/status/1994953874432823632?s=20

TanStack Startについて

Next.jsと比較したとき、まず問題として挙がるのはuse clientの存在でしょう。
コードを書く時にクライアントとサーバーの境界線を考える必要があるのは当然ですが、フレームワークへのロックインが強く、クライアントコンポーネントとの依存を常に考慮する必要があり、開発体験が良いとは言い切れない状況でした。

TanStack StartのドキュメントExecution Modelには以下の記載があります。
(本件とは関係ありませんが、TanStackのドキュメントはマークダウン記法でコピーできるUIが付いているのが素敵ですね)

All code in TanStack Start is isomorphic by default - it runs and is included in both server and client bundles unless explicitly constrained.

TanStack Start のコードは、デフォルトでアイソモーフィック(同一コードがサーバーとクライアントの両方で実行される)です。
明示的に制限しない限り、サーバー・クライアントどちらのバンドルにも含まれ、どちらでも動作します。

// ✅ This runs on BOTH server and client
function formatPrice(price: number) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(price)
}

// ✅ Route loaders are ISOMORPHIC
export const Route = createFileRoute('/products')({
  loader: async () => {
    // This runs on server during SSR AND on client during navigation
    const response = await fetch('/api/products')
    return response.json()
  },
})

実行境界(Execution Boundary)

TanStack Start のアプリケーションは、2つの環境で動作します。

サーバー環境

  • Node.js ランタイム(ファイルシステム・データベース・環境変数にアクセス可能)
  • SSR 時 — 初期ページはサーバーで描画
  • API リクエスト — サーバー側で関数が実行される
  • ビルド時 — 静的生成やプリレンダリング

クライアント環境

  • ブラウザ ランタイム(DOM、localStorage、ユーザー操作にアクセス可能)
  • ハイドレーション後 — 初期描画後にクライアントがアプリを引き継ぐ
  • ナビゲーション時 — ルートローダーがクライアント側で実行される
  • ユーザー操作 — イベントハンドラやフォーム送信など

詳しくはドキュメントを見てください。他のライブラリ・フレームワークの選択肢がある中でまだ体験したことがない人はこれを機に試してみましょう。
Better-T-Stackのおかげで導入ハードルはかなり低いはず。

TanStack Startを選んだ理由

「Next.js以外で」となると昨今は様々な選択肢があります。

フレームワーク 感想
Nuxt.js よいフレームワークだとは思うが、Vue.js自体のSyntaxが個人的にあまり好きじゃないのと、時々予期せぬ動作をしてハック的に解決しなければいけないことが何度かあったので良い思い出がない。(最近触っていないのでもしかしたら今は改善されてるかも)
Vue.js 同上
Svelte Nuxt, Vueとほぼ同じ理由。クセの強さを感じてしまった。
Solid 仮想DOMを利用しないだったり、シンプルで素晴らしいと思う反面Destructuring propsの説明を読んで「ええ。。。」となってしまった。(否定するわけではなく、個人的に苦手なだけ)
React Router 元々Remix自体が大好きだったので選択肢としては最良に近いが、Remix3が出ると言われているのにあえて選ぶ理由がなかった。

そもそもですが、ライブラリやフレームワークの選定は開発規模やチームメンバー、はたまたビジネスサイドが求める要件によっても大きく左右されます。
そのため、「◯◯が絶対的に優れている」のような意見はあまり意味がないと個人的に考えます。

oRPC最高

oRPCによる開発体験が最高です。従来のRPCは関数呼び出しによる型安全なAPIリクエストの体験が素晴らしいのですが、APIドキュメントの生成が難しいことが難点としてありました。

oRPCはRPCの開発体験に加えて、OpenAPI準拠の仕様 (エンドポイント定義や型情報) を自動生成してくれるため、フロントエンドとバックエンドでの型情報を、openapi.yamlを介さずに共有することが可能です。

手元で開発環境を動かしている方は、packages/api/src/routers/index.tsにrouteを追加してhttp://localhost:3000/api-reference へアクセスするとAPIドキュメントが動的に更新されていることが確認できるはずです。
最高ですね。

何故HonoじゃなくてElysiajs?

弊社では元々Honoを導入していたこともありhono-openapi@hono/zod-openapiを利用していたのですが、より良い開発体験を求めてElysiajsを試してみました(Honoには大変お世話になっており、とても満足しております)。
日本語的にはイリズィア(ɪˈlɪʒiʌ)jsと発音するのが良さそうです(気になる人は「elysia pronunciation」で検索してください)。

試してみようと思った主なきっかけは、ほんの少しコードを加えるだけでOpenAPI対応が可能になるシンプルさに惹かれたためです。

import { Elysia } from 'elysia'
+ import { openapi } from '@elysiajs/openapi'

new Elysia()
+	.use(openapi()) 

https://elysiajs.com/patterns/openapi

Honoで書くとこんな感じです。

import { Hono } from 'hono'
import { describeRoute, resolver, validator } from 'hono-openapi'

const app = new Hono()

app.get(
  '/',
  describeRoute({
    description: 'Say hello to the user',
    responses: {
      200: {
        description: 'Successful response',
        content: {
          'text/plain': { schema: resolver(responseSchema) },
        },
      },
    },
  }),
  validator('query', querySchema),
  (c) => {
    const query = c.req.valid('query')
    return c.text(`Hello ${query?.name ?? 'Hono'}!`)
  }
)

https://hono.dev/examples/hono-openapi#_2-create-routes

私はHonoが大好きなので特に不満はありませんが、Elysiajsくらいシンプルだとやっぱり気持ちいいですよね。


Elysiaについての詳しい説明は、公式のAt a glanceKey Conceptに譲りますが、Bunを前提とした設計になっていて、非常に高速で型安全であり、開発体験を重視しているそうです。


https://elysiajs.com/at-glance.html#performance より引用

さいごに

今回TanStack Startの利用については「今後に期待できそう」という感触でした。(もちろん一緒に開発するメンバーの懸念材料になるようでしたら採用しません)

個人的にuse clientディレクティブの呪縛から解放されるのはとても待ち望んでいたことなので、これを機に小規模なアプリから試していきたいと思います。
また、Turborepoによるmonorepo構成は以下の観点から今後AIによる開発を中心としていく際にメリットが大きそうなので採用したいと思います。

  • monorepo+RPCにすることでフロント/サーバーで実際のAPI仕様と実装の乖離がなくなるため、AIと人間の間でほぼ共通の認識を持つことが期待できる。
  • --add-dirなどが不要で、CLAUDE.md/AGENTS.mdをRepository全体で共通化して管理が楽になる(サブエージェントやSkillsで責務分担をする前提)。

※ただし、monorepo運用のオーバーヘッドや既存Repositoryとの結合によるコストは考慮していません

宣伝

アップルワールドではモダンな技術推進を行ってくれるメンバーを常に募集しているため、旅行業界での開発にチャレンジしてみたい方はぜひご連絡ください!
※採用サイトの更新が追いついていませんが、フロント、バックエンド、インフラ、PMなど、全ポジション募集中です!
https://appleworld.co.jp/career


余談

本当はState of JavaScript2025の結果が出てからこの記事を書きたかったのですが、公開されなかったため諦めました。
あくまで個人的な推測ですが、2024年はNext.jsが猛威を振るっていたので2025年もそんなに変わらないと思います

参考文献

アップルワールド Tech Blog

Discussion