2025/02/15

Next.jsとCloudflareで個人開発最強スタックを夢見る

Next.jsを中心に出来るだけお金をかけずに開発したい向けの構成です。

はじめに

個人開発してみたいけど、どの構成でしようか迷っている人向けの記事になります。
また、D1を使いたいけど実際にNext.jsからどう使えばいいかわからない人も見ていただけたらと思います。

皆大好きNext.jsを中心に、Cloudflare D1を使うケチケチ構成を紹介します。 また、Cloudflare Workersを使うことで収益化も狙える形になっています。

正直、楽さで言ったらVercelにデプロイするのがいいと思います。 ただ、Vercelの無料プランでは営利目的で使えないという弱点があります。
収益化の予定がないまたはDBを使わないプロジェクトは全然Vercelで良いと思います。
ちなみに、このブログもVercelにデプロイしています。(DBは使用していません。)

Cloudflare Workersにデプロイするとしても、要件によってはSupabase等を選択するのも全然良いと思います。

この記事を読むにあたって、今回の構成のサンプルプロジェクトがあるので実際のコードと照らし合わせながら見るとわかりやすいと思います。

PaaS

冒頭でも紹介した通り、Cloudflare Workersを使用します。
どうせCloudflare Workersにデプロイするならドメインの管理もCloudflareで一緒にやるといいと思います。

フレームワーク

タイトルにある通り、Next.jsを使います。バックエンドも含めたフルスタックな使い方をする前提です。

Next.jsをVercel以外にデプロイする場合は様々な制約があります。
それを解決するためにOpenNextというプロジェクトがあります。 現在はAWS、Cloudflare、Netlifyで使うことができます。 今回はその中のCloudflareを使用する形になります。

CloudflareでNext.jsをデプロイするとなると今まではnext-on-pagesが主流でしたが、 今はOpenNextがかなり来ています。開発もかなり活発です。

具体的なセットアップの仕方はドキュメントをご覧ください。

今回のサンプルプロジェクトではCloudflare KVでのキャッシュの機能を使っていないので、ご留意ください。

CloudflareのBindings(DBやKV等)を取得するためにgetCloudflareContextという関数が用意されています。
next-on-pagesで言うgetRequestContext相当になります。

バックエンド

Next.jsのフルスタック構成ですが、API RoutesでHonoを使用します。

フルスタックフレームワークと言ってもNext.jsのバックエンドはまだ弱いので、Honoの力を借ります。
Hono RPCを使えるのが大きなメリットになります。 加えてビルトインミドルウェアが多く、カスタムミドルウェアが作りやすいこともメリットになってきます。

最小構成だと、以下のような形で使用できます。

src/app/api/[[...hono]]/route.ts
import { Hono } from 'hono';
import { handle } from 'hono/vercel';

const app = new Hono()
  .basePath('/api')
  .get('/', (c) => {
    return c.json({ message: 'Hello, World!' });
  });

export type AppType = typeof app;

export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
export type AppType = typeof app;

こちらをクライアントから呼び出すことで型安全に開発することできます。

import type { AppType } from '@/app/api/[[...hono]]/route';
import { hc } from 'hono/client';

const client = hc<AppType>('/');

const fetchMessage = async () => {
  const res = await client.api.$get();
  return (await res.json()).message;
};

Hono RPCの詳細はドキュメントをご覧ください。

注意点としては、Bindingsを取得する際にgetCloudflareContextを使用する必要があることです。 HonoのBindingsだと正常に取得できませんでした。

ORM

今回はDrizzleを使用します。容量が小さいのでWorkersと相性が良いです。

migrationは後述するwranglerのコマンドで行うので、generateだけできるように設定しておきます。

drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/lib/schema.ts',
  dialect: 'sqlite',
});

migrationsファイルの生成は以下のコマンドで行ます。--nameは適宜変えてください。

bunx drizzle-kit generate --name=init

DB

こちらも冒頭でも話しましたが、Cloudflare D1を使用します。

まずwranglerでd1を作成します。

bunx wrangler d1 create next-stack

ここで、migrations_dirというプロパティを増やして、wrangler.jsocに追加します。

{
  ...
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "next-stack",
      "database_id": "e3271ec8-b866-4811-a035-f087cf3f0b1a",
      // 追加
      "migrations_dir": "drizzle"
    }
  ]
}

マイグレーションは以下のコマンドで行います。

# ローカル(dev環境)
bunx wrangler d1 migrations apply next-stack --local
# リモート(本番環境)
bunx wrangler d1 migrations apply next-stack --remote

これで、先ほど作成したmigrationsファイルが適用されます。

認証

個人的に今TypeScriptで一番アツい認証ライブラリです。

Auth.jsでもいいと思いますが、Better Authの方が簡単に実装できます。
Auth.jsからBetter Authに移行する際のガイドもあったりします。

プラグインが多いのが特徴で、認証に関わる大体の機能を簡単に実装できます。

基本的にドキュメント通り実装すれば問題ないですが、1つ注意点があります。
D1インスタンスは毎回getCloudflareContextから取得する必要があるということです。

なので、以下のようにする必要があります。(ドキュメントのセットアップと比較してください。)

src/lib/db.ts
import { drizzle } from 'drizzle-orm/d1';
import { schema } from './schema';

export const getDB = (db: D1Database) => {
  return drizzle(db, { schema });
};
src/lib/auth.ts
export const getAuth = (db: D1Database) => {
  return betterAuth({
    baseURL: process.env.BETTER_AUTH_URL,
    database: drizzleAdapter(getDB(db), {
      provider: 'sqlite',
    }),
    socialProviders: {
      discord: {
        clientId: process.env.DISCORD_CLIENT_ID as string,
        clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
      },
    },
  });
};
src/app/api/[[...hono]]/route.ts
import { getAuth } from '@/lib/auth';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import { Hono } from 'hono';
import { handle } from 'hono/vercel';

const app = new Hono()
  .basePath('/api')
  .on(['GET', 'POST'], '/auth/**', (c) => {
    const { env } = getCloudflareContext();
    const auth = getAuth(env.DB);
    return auth.handler(c.req.raw);
  })

export type AppType = typeof app;

export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);

もしバックエンドにHonoを使わない場合はBindings取得の関係でtoNextJsHandlerは使用できないので、自前でhandlerを実装する必要があります。 (自前といってもD1インスタンス取得してheader突っ込むくらいですが。。)

Honoを使えばAPI Routesのミドルウェアの実装もめっちゃ簡単にできます。

middleware.ts
import { getAuth } from '@/lib/auth';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import type { Session, User } from 'better-auth';
import { createMiddleware } from 'hono/factory';

export const authMiddleware = createMiddleware<{
  Variables: {
    session: Session;
    user: User;
  };
}>(async (c, next) => {
  try {
    const { env } = getCloudflareContext();
    const auth = getAuth(env.DB);

    const session = await auth.api.getSession({ headers: c.req.raw.headers });
    if (!session) {
      return c.json({}, 401);
    }

    c.set('session', session.session);
    c.set('user', session.user);
    await next();
  } catch {
    return c.json({}, 500);
  }
});

最後に

実際のコード例を出しつつ紹介しました。
サンプルプロジェクトに全てコードはあるので、そちらを見てもらうとわかりやすいとは思います。
特に、Better AuthをCloudflare D1で使うとなると、 ドキュメントのままセットアップしても動かないので、良かったらパクってください。

注意点としては、FreeプランだとWorker sizeの上限が3MBなので、かなりギリギリです。このサンプルコードで2547.45 KiBです。
ただ、Paidプランにすると上限10MBで$5+従量課金なので、Vercelよりは安くなるかなと思います。

また、決済に関してはOpenNextやBetter AuthドキュメントにStripeについての記述があるので、そちらをご覧ください。

もしよろしければ、この構成を試してみてください。

ではまた。

最終更新日:2026/01/20