MENU

問い合わせ


    【Next.jsで作るオンライン予約サービス 第0回】完成像とNext.js 16の雛形を動かす(App Router・PostgreSQL・Drizzle)

    貸し会議室やレンタルスペース、教室、サロン。電話とメールで受けている予約を、スマホから空き時間を選んで支払いまで済ませられる形にしたい。そう考える事業者は多く、予約サービスは発注相談でも定番のテーマです。この連載では、Next.js 16 で貸し会議室のオンライン予約サービスを作ります。

    「予約をネットで受けたいけれど、二重予約や決済のトラブルが怖い。どこまで自分たちで理解しておけばいいのだろう」

    結論から言うと、予約サービスの難しさは画面よりも「同じ時間に2人が予約しない仕組み」と「支払いと予約の整合」にあります。 Next.js は画面とサーバー処理を1つのプロジェクトにまとめて書けるため、小さく始めやすい技術です。第0回では、完成像とスタックの選定理由を整理し、雛形を起動してヘルスチェックとテストまで通します。

    読者は、発注側の社内エンジニア・Web担当と、これから予約サービスを依頼する発注者の方を想定しています。これまでの連載(Nuxt・Laravel・FastAPI・Rails)はお読みでなくても追えます。直前の連載は、Rails 8で作る見積・請求管理システムでした。

    目次

    オンライン予約サービスで作るもの(完成像)

    連載の最後には、利用者が会議室を探して予約・支払いまで行え、運営側が管理画面で予約を確認できるサービスになります。

    回作る機能
    1〜2スペース一覧・詳細、空き状況カレンダー
    3〜4予約フォーム、ダブルブッキング(二重予約)の防止
    5会員ログイン(メールリンク)
    6〜7Stripe での支払い、Webhook による予約の確定
    8確認メール、キャンセルと返金
    9〜11管理画面、テストの自動化、Vercel への公開

    予約サービスで事故が起きやすいのは、次の3か所です。

    • 二重予約:ほぼ同時に2人が同じ時間を押したとき、両方通ってしまう
    • 支払いと予約のずれ:支払いは済んだのに予約が確定していない、またはその逆
    • 個人情報とカード情報:氏名・連絡先の扱い、カード番号を自社で持たない設計

    連載では、この3つを順に潰していきます。

    なぜNext.js 16を選ぶのか:予約サービス向きの3つの理由

    Next.js 16 は、npm の記録では2025年10月22日に最初の版が公開されています(執筆時点の2026年10月は、16系の最新版を使用)。

    📰 出典:Next.js 16(Next.js 公式ブログ)

    理由1:画面とサーバー処理を1つにまとめて書ける

    Next.js は React(画面を部品で組み立てる技術)をもとにしたフレームワークです。App Router(URLとフォルダを対応させる仕組み)では、画面の部品を「サーバー側で描画する部品(Server Components)」として書けます。ブラウザに送るJavaScriptが減り、表示が軽くなります。

    予約の送信なども、別にAPIを作らず、サーバー上の関数(Server Actions)として書けます。第3回で扱います。

    理由2:検索に出したいページを作りやすい

    スペースの一覧や詳細ページは、検索から来てもらうためのページでもあります。サーバー側で完成した状態のHTMLを返せるため、タイトルや説明文(メタデータ)を整えやすい構成です。

    理由3:公開先の選択肢が広い

    Next.js を作っている Vercel のほか、コンテナ(Docker)でも動かせます。連載の最終回で Vercel に公開し、これまで扱ったECS・Cloud Run・Kamalとの違いも整理します。

    技術選定:データベースはPostgreSQL+Drizzle

    予約システムの核心は「同じ時間枠を二重に取らせない」ことで、これはデータベース側の仕組みで守るのが確実です。そのため PostgreSQL 16 を使います。PostgreSQL には、時間の範囲が重なる行を登録させない「排他制約」という機能があります(第4回で使います)。

    データベースを TypeScript から安全に扱う道具には Drizzle ORM を選びました。ORM とは、データベースの表をプログラム上の部品として扱う仕組みです。SQLに近い書き方ができ、排他制約のようなデータベース固有の機能とも組み合わせやすいのが理由です。

    雛形を作る

    検証環境は Node.js 22系です。次の部品を入れます。

    npm i next@16 react@19 react-dom@19 drizzle-orm postgres
    npm i -D typescript eslint eslint-config-next vitest drizzle-kit @types/node @types/react @types/react-dom

    package.json の scripts には、連載を通して使う5つのコマンドを置きます。

    {
      "scripts": {
        "dev": "next dev",
        "build": "next build",
        "lint": "eslint .",
        "typecheck": "tsc --noEmit",
        "test": "vitest run"
      }
    }

    トップページは、まず文字を出すだけの Server Component にします。

    // app/page.tsx
    export default function Home() {
      return (
        <main>
          <h1>スペース予約サービス</h1>
          <p>貸し会議室を、スマホから空き時間を選んで予約・お支払いまで行うサンプルです。</p>
        </main>
      );
    }

    "use client" を書いていないので、この部品はサーバー側で描画されます。

    PostgreSQLをDocker Composeで用意する

    データベースは、Docker Compose(複数のサーバーをまとめて起動する道具)で用意します。

    # docker-compose.yml
    services:
      db:
        image: postgres:16
        environment:
          POSTGRES_USER: yoyaku
          POSTGRES_PASSWORD: yoyaku
          POSTGRES_DB: yoyaku
        ports:
          - "5432:5432"

    接続先は環境変数 DATABASE_URL から読み、コードにパスワードを書きません。見本は .env.example に置き、実際の .env は Git に入れません。

    // lib/db.ts
    import { drizzle } from "drizzle-orm/postgres-js";
    import postgres from "postgres";
    
    export function createDb() {
      const url = process.env.DATABASE_URL;
      if (!url) {
        throw new Error("DATABASE_URL が設定されていません(.env.example を参照)");
      }
      const client = postgres(url, { max: 1 });
      return { db: drizzle(client), client };
    }

    ここで使っている yoyaku というパスワードは、手元で試すためだけの値です。本番では別の強い値を環境変数で渡します。

    ヘルスチェックとテストを最初から置く

    「サーバーとデータベースが生きているか」を返すURLを、最初に作っておきます。公開後の監視や、デプロイ後の確認で使います。

    // lib/health.ts
    export async function checkDatabase(ping: () => Promise<unknown>) {
      try {
        await ping();
        return { ok: true, database: "up" as const };
      } catch {
        return { ok: false, database: "down" as const };
      }
    }
    // app/api/health/route.ts(抜粋)
    export async function GET() {
      const { db, client } = createDb();
      const result = await checkDatabase(() => db.execute(sql`select 1`));
      await client.end();
      return Response.json(result, { status: result.ok ? 200 : 503 });
    }

    データベースへの問い合わせを関数として外から渡す形にしているのは、実際のデータベースが無くてもテストできるようにするためです。

    // tests/health.test.ts(抜粋)
    it("DBが応答しなければ ok=false", async () => {
      const result = await checkDatabase(async () => {
        throw new Error("connection refused");
      });
      expect(result).toEqual({ ok: false, database: "down" });
    });

    動作確認

    次のコマンドを実行します。

    npm run lint
    npm run typecheck
    npm test
    npm run build

    私の検証環境では、lint の指摘は0件、型チェックは成功、テスト2件が成功し、npm run build も成功しました(/ は静的ページ、/api/health は都度サーバーで処理するページとして出力されます)。

    さらに、手元のPostgreSQL 16を起動して DATABASE_URL を渡し、npm start で起動したうえで確認しました。トップページに見出しが表示され、/api/health は {"ok":true,"database":"up"}(HTTP 200)を返しました。

    なお、この環境ではDockerが使えなかったため、docker-compose.yml そのものの起動は未確認です。上のとおり、同じバージョンのPostgreSQLを直接起動して動作を確認しています。

    つまずきやすい点

    • Nodeのバージョン:Next.js 16 は新しめのNode.jsが前提です。古いバージョンだと起動前に止まります。公式の要件を確認してください。
    • DATABASE_URL の設定漏れ:未設定だと /api/health は 503 を返します。.env を作って読み込んでいるか確認します。
    • 5432番ポートの競合:すでにPostgreSQLが動いていると、Composeのデータベースが起動しません。

    発注者向けメモ:予約サービスを頼むときの確認点

    この連載の内容を、開発会社に予約サービスを依頼する立場で見ると、次の点を確認しておくと安心です。

    • ☐ 二重予約をどの仕組みで防ぐのか(画面だけでなく、データベース側でも防ぐか)説明があるか
    • ☐ 支払いは Stripe などの決済サービスに任せ、カード情報を自社のサーバーで持たない設計か
    • ☐ 支払い完了を受け取れなかったときの扱い(仮予約の期限、再確認の方法)が決まっているか
    • ☐ キャンセル・返金のルールを、サービス側で先に決めてから見積もりを依頼しているか
    • ☐ 個人情報(氏名・連絡先)の保管場所と、削除の方法が確認できるか
    • ☐ 本番環境の月々の費用(ホスティング・データベース・決済手数料)の内訳が示されるか

    決済や個人情報に関する法令の扱いは、実際のサービス内容によって異なります。個別の判断は、専門家や各省庁・事業者の公式窓口でご確認ください。

    また、この回ではStripeでの支払い、メール送信、Vercelへの公開は扱っておらず、未確認です。該当する回で、確認できた範囲と未確認の範囲を明記します。

    まとめと次回予告

    第0回では、予約サービスの完成像と、Next.js 16・PostgreSQL・Drizzleという構成の理由を整理しました。そのうえで雛形を作り、トップページ、ヘルスチェック、テストまで通しました。

    次回の第1回は「スペース一覧と詳細ページを作る」です。Drizzleで表を定義し、Server Components で一覧を表示します。

    この連載の記事一覧

    この記事は連載「Next.jsで作るオンライン予約サービス」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    • 【Next.jsで作るオンライン予約サービス 第0回】完成像とNext.js 16の雛形を動かす(App Router・PostgreSQL・Drizzle)(この記事)

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      目次