貸し会議室やレンタルスペース、教室、サロン。電話とメールで受けている予約を、スマホから空き時間を選んで支払いまで済ませられる形にしたい。そう考える事業者は多く、予約サービスは発注相談でも定番のテーマです。この連載では、Next.js 16 で貸し会議室のオンライン予約サービスを作ります。
「予約をネットで受けたいけれど、二重予約や決済のトラブルが怖い。どこまで自分たちで理解しておけばいいのだろう」
結論から言うと、予約サービスの難しさは画面よりも「同じ時間に2人が予約しない仕組み」と「支払いと予約の整合」にあります。 Next.js は画面とサーバー処理を1つのプロジェクトにまとめて書けるため、小さく始めやすい技術です。第0回では、完成像とスタックの選定理由を整理し、雛形を起動してヘルスチェックとテストまで通します。
読者は、発注側の社内エンジニア・Web担当と、これから予約サービスを依頼する発注者の方を想定しています。これまでの連載(Nuxt・Laravel・FastAPI・Rails)はお読みでなくても追えます。直前の連載は、Rails 8で作る見積・請求管理システムでした。
オンライン予約サービスで作るもの(完成像)
連載の最後には、利用者が会議室を探して予約・支払いまで行え、運営側が管理画面で予約を確認できるサービスになります。
| 回 | 作る機能 |
|---|---|
| 1〜2 | スペース一覧・詳細、空き状況カレンダー |
| 3〜4 | 予約フォーム、ダブルブッキング(二重予約)の防止 |
| 5 | 会員ログイン(メールリンク) |
| 6〜7 | Stripe での支払い、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)(この記事)









