Cloudflare Workers & Pages×D1とNext.jsで作るCRUD WebAPI

普段のお仕事に役立つ普遍的なプログラミングTIPSや、業界で注目度が高い最新情報をお届けする「編集部ピックアップ」。
今回は、フルスタックなWebアプリケーションを簡単に構築できるクラウドサービスプロバイダー「Cloudflare」と、そのサービスの活用例についてご紹介します。

はじめに

Cloudflare」というクラウドサービスプロバイダをご存知でしょうか?世界中の大手ECサイトやニュースメディア、SNSなど、多くのWebサービスで利用されているため、名前だけは耳にしたことがあるという方も少なくないかもしれません。

Cloudflareとは

Cloudflare公式サイト

Cloudflareとは、もともと「CDN(コンテンツ配信ネットワーク)」や「WAF(Web Application Firewall)」など、Webサイトの高速化やセキュリティ強化を目的としたインフラサービスを提供する企業として設立されました。
Cloudflareをご存知の方は、こうしたCDNやWAFといったサービスをまず思い浮かべるのではないでしょうか。

しかし、現在のCloudflareはそれらのインフラ面だけのサービスにとどまりません。

  • 静的ファイルもフロントアプリもホスティングできる「Pages
  • JavaScriptやTypeScriptでサーバーレスAPIを構築できる「Workers
  • SQLiteベースの軽量なサーバーレスSQLデータベース「D1
  • Amazon S3互換のオブジェクトストレージ「R2
  • セッションやCookieの管理もできる「KV」、「Durable Objects
  • AIアプリケーション開発の為の「AI Gateway」、「Vectorize」、「Workers AI」など…

Webアプリ開発に必要なすべてが揃っているフルスタックプラットフォームへと進化しています。

今回はこのCloudflareの中でも代表的な機能である、Pages、Workers、そしてD1を組み合わせて、Next.jsを使いながら簡単なCRUD処理が行える、WebAPIの作成について解説していきます。

Cloudflareをつかってみる

CloudflareでWebAPIを作成していくにあたって、まず、Cloudflareのアカウントを準備してください。今回は無料プランで試していきます。

アカウント作成後、トップページよりログインしてみると次のようにダッシュボードページが表示されます。左側のメニューにはさまざまなサービスが表示されており、こちらからインフラサービスや、Webアプリケーションの作成などが行えます。

Cloudflareダッシュボード

Webアプリケーションの作成にはWorkers & Pagesを利用します。これを選択すると、テンプレートを使用した作成や、GitHubリポジトリからのインポートが可能で、すばやくWebアプリケーションを構築するための機能が利用できます。

Cloudflare Worker&Pages

データベースを利用したい場合は、D1 SQLデータベースを利用します。画面上から、データベースの作成を行うことが可能で、テーブルの追加、データの追加なども行えます。

Cloudflare d1sql

CRUD Web APIの作成

Web上からも、Webアプリケーションの作成や、データベースの作成など一通りの機能が使えることが分かりましたが、今回作成する、WebAPIは、VSCodeとCloudflareが提供するCLIツールを利用して作成していきます。

開発環境

  • VSCode(Visual Studio Code
  • Node.js ※ LTS (長期サポート) バージョンのご利用をお勧めします
  • Cloudflare C3(create-cloudflare CLI)
    Next.jsをはじめとする、さまざまなJSフレームワークのWebアプリケーションプロジェクトを作成できる、スキャフォールディング対応のCLIツールです。Workers & PagesのWebアプリケーションプロジェクト作成に利用します。
  • Cloudflare Wrangler ※ C3を実行すると、自動的にインストールされます
    Cloudflareの開発者向けCLIツールです。D1にデータベースを作成したり、WorkersやPagesなどのリソースのデプロイ、管理、設定変更を行うために利用します。ローカル環境での開発やプレビュー機能にも対応しており、Cloudflare開発プラットフォームを効率的に操作できます。

プロジェクトの作成

それでは、実際にアプリケーションを作成していきます。まずはじめに、Cloudflare C3のコマンドを利用してプロジェクトを作成します。任意のフォルダでVS Codeを開き、ターミナルを起動して以下のコマンドを実行してください。

npm create cloudflare@latest

コマンドを実行すると、ダイアログ形式で実行確認が行われ、「y」を押すとプロジェクトの設定が進みます。

C3-1

続いて、プロジェクト名の設定です。今回は「cf-crud-app」と設定しました。

C3-2

つぎに、どのテンプレートから始めるかを選ぶダイアログが表示されます。今回は「Framework Starter」を選択。

C3-3

つづいて、フレームワークの選択です。今回は「Next.js」を選択します。

C3-4

Next.jsを選択すると、Next.jsのCLI「create-next-app」が実行されます。「create-next-app」のダイアログによって、Next.jsアプリケーションに関する設定が行われます。選択した設定値は以下の通りです。

C3-5

つぎに、Git管理を行うかの確認です。「Yes」を選択します。

C3-6

さいごに、このアプリをCloudflare上にデプロイするかを確認されます。まだ、コード実装を行っていない為、「No」を選択します。ここまででプロジェクト作成は終了です。

C3-7

swagger-ui-reactのインストール

今回作成するWeb APIの動作確認には、Swagger UIを利用します。Swagger UIのJSライブラリ「swagger-ui」のReact向けコンポーネント「swagger-ui-react」をプロジェクトにインストールし、Swagger UIを導入しますが、2025年5月現在「swagger-ui-react」はReact 19に対応していないため、まずReactをバージョン18.3.1にダウングレードします。

cd cf-crud-app   #プロジェクトフォルダに移動していない場合は移動してください。
npm install react@18.3.1 react-dom@18.3.1

つづいて、「swagger-ui-react」をインストールします。

npm install swagger-ui-react

以下のコマンドで型定義ファイルもインストールします。

npm install  @types/swagger-ui-react

D1データベースの作成

つづいて、CRUD処理の対象となるデータベースをD1上に作成していきます。

D1のデータベースを作成するには、Cloudflareが提供するCLIツール「Wrangler」を利用します。

次のWranglerコマンドを実行すると「prod-d1-crud」という名前のデータベースが作成されます。

npx wrangler d1 create prod-d1-crud
D1データベースの作成1

CLIに対する認証が行われていない場合は、コマンド実行時にブラウザが自動的に起動し、対象のCloudflareアカウント環境の操作を許可するかどうかを確認する認証ダイアログが表示されます。ここで「Allow」をクリックすることで、Wrangler CLIに対して認証と権限付与が完了します

D1データベースの作成2

VSCodeに戻ってターミナルを確認すると、データベースが作成されたことを示すメッセージが表示されています。また、作成されたデータベースの情報も出力されています。

D1データベースの作成3

アプリケーションから、DBをバインドするために、出力されていたDBの情報を次のように「wrangler.jsonc」ファイルへ更新します。

D1データベースの作成4

ここまでで、データベースの作成は完了しました。つづいてテーブルの作成や、データを挿入するための以下のSQLファイルをプロジェクトフォルダの直下に作成します。

DROP TABLE IF EXISTS Invoices;

CREATE TABLE IF NOT EXISTS Invoices (
    ID INTEGER PRIMARY KEY,
    BillNo TEXT NOT NULL,
    SlipNo TEXT NOT NULL,
    CustomerID TEXT NOT NULL,
    CustomerName TEXT NOT NULL,
    Products TEXT NOT NULL,
    Number INTEGER NOT NULL,
    UnitPrice DECIMAL NOT NULL,
    Date DATE
);
INSERT INTO
    Invoices (ID,BillNo,SlipNo,CustomerID,CustomerName,Products,Number,UnitPrice,Date)
VALUES
    (1,'WS-DF502','GB465','2ADFG','コンビニエンス北風','コーヒー 250 ml',100,100,'2020-01-05T00:00:00'),
    (2,'WS-DF502','GB465','2ADFG','コンビニエンス北風','紅茶 350 ml',300,120,'2020-01-05T00:00:00'),
    (3,'WS-DF502','DK055','2ADFG','コンビニエンス北風','炭酸飲料 (オレンジ) 350 ml',200,120,'2020-01-09T00:00:00'),
    (4,'DB-00001','7948352','1ADZA','みのり商事','モッツァレッラチーズとトマトのサラダ',1,1200,'2019-02-04T00:00:00'),
    (5,'DB-00001','7948352','1ADZA','みのり商事','モッツァレッラチーズとトマトのサラダ',119,1200,'2019-02-04T00:00:00'),
    (6,'DB-00001','1797382','1ADZA','みのり商事','ナポリ風スパゲティー',1,2500,'2019-02-04T00:00:00'),
    (7,'DB-00001','1797382','1ADZA','みのり商事','ナポリ風スパゲティー',59,2500,'2019-02-04T00:00:00'),
    (8,'DB-00001','9963274','1ADZA','みのり商事','小羊背肉のリンゴソースかけ',1,5000,'2019-02-04T00:00:00'),
    (9,'DB-00001','9963274','1ADZA','みのり商事','小羊背肉のリンゴソースかけ',129,5000,'2019-02-04T00:00:00'),
    (10,'DB-00002','8398400','1BCDS','居酒屋ななべえ','超極厚牛タン',1,4800,'2019-02-08T00:00:00'),
    (11,'DB-00002','8398400','1BCDS','居酒屋ななべえ','超極厚牛タン',129,4800,'2019-02-08T00:00:00'),
    (12,'DB-00002','7079356','1BCDS','居酒屋ななべえ','濃厚ずんだロール',20,780,'2019-02-08T00:00:00'),
    (14,'DB-00003','7627259','3AXDS','レストラン石坂','マスクメロン',90,1100,'2019-02-18T00:00:00'),
    (15,'DB-00003','8238079','3AXDS','レストラン石坂','ラ・フランス',150,700,'2019-02-18T00:00:00')
;

SQLファイルの作成後、以下のコマンドでローカル環境に実行します。

npx wrangler d1 execute prod-d1-crud --local --file=./schema.sql

以下コマンドで動作確認を行います。

npx wrangler d1 execute prod-d1-crud --local --command="SELECT * FROM Invoices"

それぞれのコマンドを実行すると、ローカル環境にテーブルが作成され、データが設定されていることも確認できます。

D1テーブルの作成

つぎに、Worker&Pages側より、D1のデータベースを参照可能にする為、以下コマンドで型定義を生成しておきます。プロジェクトフォルダに「worker-configuration.d.ts」が生成されていれば成功です。

npx wrangler types

CRUD処理の実装

プロジェクト作成、データベースの準備ができましたのでコードを実装していきます。

今回はNext.jsのApp Routerを使用して構築するため、「app」フォルダ配下に「api」フォルダを作成します。次に、CRUD処理の対象となる「Invoices」テーブルと同名のフォルダをその中に作成し、そのフォルダ内に「route.ts」ファイルを配置してコードを実装します。

ファイル構成

route.tsには、次のコードのように、CRUD処理の「Read(読み取り)処理」を、GET処理で実装。「Create(作成)処理」をPOST処理にて実装します。

import {NextRequest, NextResponse } from 'next/server';
import {getCloudflareContext } from '@opennextjs/cloudflare';

// Next.js Edge Runtime の設定
// この設定により、APIルートが Cloudflare Edge 上で実行されます。
export const config = { runtime: 'edge' };

/**
 * Cloudflare のコンテキストからデータベース接続オブジェクトを取得する関数
 * - Cloudflare 環境変数 (env) から DB を取り出す
 * - DB が存在しない場合、コンテキスト情報を含むエラーをスローする
 */
function getDB(): D1Database {
    // Cloudflare 用のコンテキストを取得
    const context = getCloudflareContext();
    // 環境変数として設定された DB オブジェクトを取得(型キャストを実施)
    const env = context.env as Env;
    // DB が存在しない場合は、コンテキスト情報をデバッグ用にエラーとして出力
    if (!env.DB) {
      throw new Error(`DB not found; context: ${JSON.stringify(context)}`);
    }
    // 正常な場合は DB オブジェクトを返す
    return env.DB;
}

/**
 * GET リクエストハンドラ
 * - クライアントからの GET リクエストを受け付け、Invoices テーブルの全レコードを取得
 * - 正常時は取得結果を JSON レスポンスとして返す
 * - エラー発生時は、エラーメッセージを含む JSON レスポンス(500 ステータス)を返す
 */
export async function GET() {
    try {
      // データベース接続を確立
      const db = getDB();
      // SQL クエリを実行し、Invoices テーブルから全レコードを取得
      const { results } = await db.prepare('SELECT * FROM Invoices').all();
      // 取得したレコードを JSON レスポンスとしてクライアントに返す
      return NextResponse.json(results);
    } catch (error) {
      // エラー発生時にエラーログをコンソールへ出力
      console.error('Error in GET:', error);
      // エラーメッセージとともに 500 ステータスの JSON レスポンスを返す
      return NextResponse.json(
        { error: error instanceof Error ? error.message : String(error) },
        { status: 500 }
      );
    }
}

/**
 * POST リクエストハンドラ
 * - クライアントからの POST リクエストを受け付け、新規の Invoice レコードをデータベースに挿入
 * - リクエストボディから billNo, slipNo, customerId, customerName, products, quantity, unitPrice, date の各パラメータを抽出
 * - 必要なパラメータがすべて存在するか検証し、不足している場合は 400 ステータスのエラーを返す
 * - 全パラメータ存在時は、INSERT クエリを実行してレコードを追加し、結果を JSON として返す
 * - エラー発生時は、エラーメッセージを含む JSON レスポンス(500 ステータス)を返す
 */
export async function POST(request: NextRequest) {
    try {
      // データベース接続を確立
      const db = getDB();
      // リクエストの詳細をコンソールに出力(デバッグ用)
      console.log('request:', request);
      // リクエストボディを JSON としてパースし、必要なパラメータを抽出
      const body = await request.json() as {
        ID: number
        BillNo: string
        SlipNo: string
        CustomerID: string
        CustomerName: string
        Products: string
        Number: number
        UnitPrice: number
        Date: string
      };
      // 分割代入により、各パラメータを変数に代入
      const {ID, BillNo, SlipNo, CustomerID, CustomerName, Products, Number, UnitPrice, Date } = body;
      // 全てのパラメータの検証
      if (
        ID === undefined || ID === null ||
        BillNo === undefined || BillNo === null ||
        SlipNo === undefined || SlipNo === null ||
        CustomerID === undefined || CustomerID === null ||
        CustomerName === undefined || CustomerName === null ||
        Products === undefined || Products === null ||
        Number === undefined || Number === null ||
        UnitPrice === undefined || UnitPrice === null ||
        Date === undefined || Date === null
      ) {
        // パラメータ不足の場合、エラーメッセージとともに 400 ステータスの JSON レスポンスを返す
        return NextResponse.json(
          { error: 'Invalid request. All parameters required: ID, BillNo, SlipNo, CustomerID, CustomerName, Products, Number, UnitPrice, Date' },
          { status: 400 }
        );
      }
      // SQL クエリを用いて Invoices テーブルへ新規レコードを挿入
      // プレースホルダを使用し、バインドにより値を安全に挿入(SQL インジェクション対策)
      const result = await db
        .prepare(
          "INSERT INTO Invoices (id, BillNo, SlipNo, CustomerID, CustomerName, Products, Number, UnitPrice, Date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
        )
        .bind(ID, BillNo, SlipNo, CustomerID, CustomerName, Products, Number, UnitPrice, Date)
        .run();
      // 挿入結果を JSON レスポンスとして返す
      return NextResponse.json({ result });
    } catch (error) {
      // エラー発生時は、エラーログをコンソールへ出力
      console.error('Error in POST:', error);
      // エラーメッセージを含む JSON レスポンス(500 ステータス)を返す
      return NextResponse.json(
        { error: error instanceof Error ? error.message : String(error) },
        { status: 500 }
      );
    }
}

つづいて、「Update(更新)」、「Delete(削除)」、「Read(1件取得)」の処理を実装するため、「Invoices」フォルダの中に「[Id]」という名前のフォルダを作成します。さらにその中に、先ほどと同様に「route.ts」ファイルを用意して各処理を実装していきます。

※ 「[id]」フォルダはApp Routerの動的ルーティング機能を利用するためのもので、URLパラメータとして特定のIDを受け取る際に使用します。

import { NextResponse, NextRequest } from 'next/server';
import { getCloudflareContext } from '@opennextjs/cloudflare';

export const config = { runtime: 'edge' };

/**
 * getDB関数は、CloudflareのコンテキストからDBを取得するヘルパー関数です。
 * DBが存在しない場合は、コンテキスト情報と共にエラーをスローします。
 */
function getDB(): D1Database {
  const context = getCloudflareContext(); // Cloudflareのリクエストコンテキストを取得
  const env = context.env as Env; // 環境設定から環境変数にキャスト
  if (!env.DB) { // DB情報が存在しない場合のチェック
    throw new Error(`DB not found; context: ${JSON.stringify(context)}`);
  }
  return env.DB; // DBを返す
}

/**
 * GETハンドラー:
 * 指定されたIDのInvoiceをデータベースから取得し、JSON形式で返します。
 */
export async function GET(request: NextRequest, { params }: { params: Promise<{ Id: string }> }) {
  try {
    const db = getDB(); // データベース接続を取得
    const id = (await params).Id; // ルートパラメータからIDを取得
    // Invoicesテーブルから指定されたIDのレコードを取得するSQLクエリを実行
    const { results } = await db.prepare('SELECT * FROM Invoices Where ID =?')
                              .bind(id)
                              .all();
    return NextResponse.json(results); // 結果をJSONで返す
  } catch (error) {
    console.error('Error in GET:', error); // エラーをログに出力
    return NextResponse.json(
      { error: error instanceof Error ? error.message : String(error) },
      { status: 500 } // サーバー内部エラーを返す
    );
  }
}

/**
 * PUTハンドラー:
 * 指定されたIDのInvoiceの情報を更新します。
 * リクエストボディに更新対象のフィールドと値を含め、SQL UPDATEクエリを動的に構築して実行します。
 */
export async function PUT(request: NextRequest, { params }: { params: Promise<{ Id: string }> }) {
  try {
    const db = getDB(); // データベース接続を取得
    const id = (await params).Id; // ルートパラメータからIDを取得
    // リクエストボディをJSONとして解析し、部分的なInvoice情報を取得
    const body = await request.json() as Partial<{
      BillNo: string;
      SlipNo: string;
      CustomerID: string;
      CustomerName: string;
      Products: string;
      Number: number;
      UnitPrice: number;
      Date: string;
    }>;
    const fieldsToUpdate: string[] = []; // 更新対象のフィールドを格納する配列
    const values: (string | number)[] = []; // SQLバインド用の値を格納する配列

    // 各フィールドが存在し、適切な値であれば対応するSQLの更新文を構築
    if (body.BillNo != null && body.BillNo.trim() !== "") {
      fieldsToUpdate.push("BillNo = ?");
      values.push(body.BillNo);
    }
    if (body.SlipNo != null && body.SlipNo.trim() !== "") {
      fieldsToUpdate.push("SlipNo = ?");
      values.push(body.SlipNo);
    }
    if (body.CustomerID != null && body.CustomerID.trim() !== "") {
      fieldsToUpdate.push("CustomerID = ?");
      values.push(body.CustomerID);
    }
    if (body.CustomerName != null && body.CustomerName.trim() !== "") {
      fieldsToUpdate.push("CustomerName = ?");
      values.push(body.CustomerName);
    }
    if (body.Products != null && body.Products.trim() !== "") {
      fieldsToUpdate.push("Products = ?");
      values.push(body.Products);
    }
    if (body.Number != null) {
      fieldsToUpdate.push("Number = ?");
      values.push(body.Number);
    }
    if (body.UnitPrice != null) {
      fieldsToUpdate.push("UnitPrice = ?");
      values.push(body.UnitPrice);
    }
    if (body.Date != null && body.Date.trim() !== "") {
      fieldsToUpdate.push("Date = ?");
      values.push(body.Date);
    }
    if (fieldsToUpdate.length === 0) {
      // 更新対象のフィールドが一つも提供されなかった場合、エラーレスポンスを返す
      return NextResponse.json({ error: 'No valid fields provided for update.' }, { status: 400 });
    }
    // 更新対象フィールドをカンマ区切りで連結し、動的にSQLのUPDATE文を構築
    const sql = `UPDATE Invoices SET ${fieldsToUpdate.join(", ")} WHERE ID = ?`;
    values.push(id); // 最後のバインドパラメータとしてIDを追加
    const result = await db.prepare(sql)
                           .bind(...values)
                           .run(); // SQLクエリを実行
    return NextResponse.json({ result }); // 結果をJSONで返す
  } catch (error) {
    console.error('Error in PUT:', error); // エラーをログに出力
    return NextResponse.json(
      { error: error instanceof Error ? error.message : String(error) },
      { status: 500 } // サーバー内部エラーを返す
    );
  }
}

/**
 * DELETEハンドラー:
 * 指定されたIDのInvoiceをデータベースから削除します。
 */
export async function DELETE(request: Request, { params }: { params: Promise<{ Id: string }> }) {
  try {
    const db = getDB(); // データベース接続を取得
    // ルートパラメータからIDを取得(コメントではCustomerIDと記載されていますが、実際にはInvoiceのIDです)
    const id = (await params).Id;

    // 指定されたIDのInvoiceを削除するSQLクエリを実行
    const result = await db.prepare('DELETE FROM Invoices WHERE ID = ?')
                        .bind(id)
                        .run();
    return NextResponse.json({ result }); // 結果をJSONで返す
  } catch (error) {
    console.error('Error in DELETE:', error); // エラーをログに出力
    return NextResponse.json(
      { error: error instanceof Error ? error.message : String(error) },
      { status: 500 } // サーバー内部エラーを返す
    );
  }
}

ここまでで、CRUD処理のAPI実装は完了です。試しに以下のコマンドでデバッグ実行を行って、動作確認を行います。

npm run preview

ブラウザで、以下のようにWebAPIを呼び出すと、正しくデータを取得することができました。

APIの動作確認

Swagger UIの導入

ここまでで、APIの実装は完了しGET処理の動作確認まで行うことができました。最後にSwagger UIからの動作確認が行えるように、処理を実装していきます。

Swagger.jsonの定義

最初に、作成したAPIの各処理に対応するSwagger定義ファイル(swagger.json)を以下のように作成します。

{
  "openapi": "3.0.0",
  "info": {
    "title": "Invoices CRUD API",
    "version": "1.0.0",
    "description": "InvoicesテーブルCRUD操作API"
  },
  "servers": [
    {
      "url": "/"
    }
  ],
  "paths": {
    "/api/Invoices": {
      "get": {
        "summary": "Invoicesテーブルの全てのレコードを取得します",
        "responses": {
          "200": {
            "description": "Retrieved list of Invoices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Invoicesテーブルにレコードを追加します",
        "requestBody": {
          "description": "Invoiceテーブルに追加するオブジェクトを設定します",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ID": {
                    "type": "number"
                  },
                  "BillNo": {
                    "type": "string"
                  },
                  "SlipNo": {
                    "type": "string"
                  },
                  "CustomerID": {
                    "type": "string"
                  },
                  "CustomerName": {
                    "type": "string"
                  },
                  "Products": {
                    "type": "string"
                  },
                  "Number": {
                    "type": "number"
                  },
                  "UnitPrice": {
                    "type": "number"
                  },
                  "Date": {
                    "type": "string"
                  }
                },
                "required": [
                  "ID",
                  "BillNo",
                  "SlipNo",
                  "CustomerID",
                  "CustomerName",
                  "Products",
                  "Number",
                  "UnitPrice",
                  "Date"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice record created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, missing parameters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/Invoices/{Id}": {
      "get": {
        "summary": "指定したIDのレコードを取得します",
        "parameters": [
          {
            "name": "Id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice record retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "指定したIDのレコードを更新します",
        "parameters": [
          {
            "name": "Id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID"
          }
        ],
        "requestBody": {
          "description": "更新する内容のフィールドと値を設定",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "BillNo": {
                    "type": "string"
                  },
                  "SlipNo": {
                    "type": "string"
                  },
                  "CustomerID": {
                    "type": "string"
                  },
                  "CustomerName": {
                    "type": "string"
                  },
                  "Products": {
                    "type": "string"
                  },
                  "Number": {
                    "type": "number"
                  },
                  "UnitPrice": {
                    "type": "number"
                  },
                  "Date": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice record updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, missing parameters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "指定したIDのレコードを削除します",
        "parameters": [
          {
            "name": "Id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice record deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

作成したファイルは、以下のように「プロジェクトフォルダ/public/」内に配置します。

swaggerjsonの配置

トップページにSwagger UIを実装

つづいて、swagger.jsonを読み込み、Swagger UIを表示するトップページの実装を行います。プロジェクト作成時にデフォルトで作成された「プロジェクトフォルダ/app/page.tsx」を以下のように書き換えます。

import SwaggerUI from "swagger-ui-react";
import "swagger-ui-react/swagger-ui.css";

export default function Home() {
  return (
    <div className="p-8">
      <h1 className="text-3xl font-bold mb-4">API Documentation</h1>
      <SwaggerUI url="/swagger.json" />
    </div>
  );
}

「swagger-ui-react」を利用し、swagger.jsonファイルを読み込むだけなので、非常にシンプルに実装できます。

動作確認

実装が完了しましたので、先ほどと同様にnpm run previewにて動作確認してみると、次のようにSwagger UIがトップページに表示されていることが確認できます。

Swagger UIの確認

Cloudflare環境へデプロイ

ローカルでSwaggerUIの起動確認もできましたので、最後にCloudflare環境へデプロイしてきます。

D1データベースにテーブル作成

データベースの作成時に、ローカル環境に対しては、テーブルの作成とデータの追加を行っていましたが、Cloudflare環境へはまだ作成していませんでしたので、先ほどのコマンドの引数を--localから--remoteに変更して実行します。

npx wrangler d1 execute prod-d1-crud --remote --file=./schema.sql

先ほどと同様に、以下のSQLを実行して、動作確認してみます。

npx wrangler d1 execute prod-d1-crud --remote --command="SELECT * FROM Invoices"

以下のように、Cloudflare環境上のデータベースにも正しくテーブルが作成され、レコードが追加されているのを確認できました。

D1データベースにテーブル追加

Cloudflare環境へデプロイ

それでは、最後にCloudflare環境へデプロイします。デプロイは非常に簡単で、以下のコマンドだけでデプロイが行われます。

npm run deploy

つぎのように、成功メッセージ後に、デプロイ先URLが表示されれば、デプロイ成功です。

デプロイ成功

動作確認

デプロイが完了しましたので、実装した動作の確認を行っていきます。

GET処理(Read-全件)

GET処理(Read-指定レコード)

POST処理(Create)

PUT処理(Update)

DELETE処理(Delete)

全ての処理が、正しく動作することが確認できました。

さいごに

今回は、Cloudflare Workers & Pages × D1を利用して、CRUD処理を行うためのWeb APIを実装してみました。CloudflareのCLIツール「C3」を活用することで、今回作成した「Next.js」に限らず、さまざまなフレームワークを利用したWebアプリケーションを非常に簡単に構築できます。また、「D1」SQLデータベースも手軽に利用できるため、業務アプリケーションの構築にも適した選択肢となるでしょう。

クラウドサービス上で業務アプリケーションを構築してみたいとお考えの方は、ぜひ本記事を参考に、一度お試しください。

このほか、本記事で作成したWeb APIを利用し、Cloudflare環境上に「Wijmo(ウィジモ)」のグリッドコンポーネント「FlexGrid」を活用したサーバーレスCRUD処理アプリの構築を解説する記事も公開しています。こちらもあわせてご覧ください。

メシウスでは業務アプリケーション開発の工数削減に役立つ高機能なJavaScriptライブラリを提供しています。

メシウスのJavaScriptライブラリ

無償のトライアル版や、ブラウザ上で手軽に試せるデモアプリケーションも公開しているので、こちらも是非ご覧ください。

\  この記事をシェアする  /