Prisma×Next.jsでMySQLと連携するWeb APIを作成してWijmoでCRUD処理を行う

「Prisma」はNode.jsとTypeScriptで使えるORM(Object-Relational Mapping)ツールです。Prismaを使用すると、SQLを直接書くことなくデータベースを操作できるので、開発者はデータベースとのやり取りをより直感的かつ安全に行うことができます。

本記事ではPrismaとNext.jsを使用してMySQLと連携するWeb APIを作成し、JavaScript UIライブラリ「Wijmo(ウィジモ)」のデータグリッドコントロール「FlexGrid(フレックスグリッド)」と連携してデータの生成(Create)、読込(Read)、更新(Update)、削除(Delete)を行うCRUDアプリケーションを作成する方法をご紹介します。

開発環境

  • Next.js 15.4.6
  • React 19.1.0
  • Prisma 6.13.0
  • Wijmo 5.20251.40

Next.jsプロジェクトの作成

以下のコマンドを実行して、ベースとなるNext.jsのアプリケーションを作成します。

npx create-next-app@latest wijmo-prisma-app

プロジェクトが作成されたら、以下のコマンドでプロジェクトの配下に移動します。

cd wijmo-prisma-app

バックエンド部分の作成

Prisma のインストールと初期化

次に以下のコマンドを実行して、PrismaとPrisma Clientをインストール、ならびにPrismaの初期化を行います。

npm install prisma tsx --save-dev
npm install @prisma/client
npx prisma init --datasource-provider mysql --output ../generated/prisma

初期化が完了すると「prisma/schema.prisma」ファイルと「 .env」ファイルが生成されます

MySQLとの接続設定とスキーマの定義

次に生成された「 .env」ファイルにMySQLへの接続設定を記載します。
※ user、password、db_nameはお使いの環境に合わせて修正してください。

DATABASE_URL="mysql://user:password@localhost:3306/db_name"

続けて、「prisma/schema.prisma」ファイルを編集し、「OrderData」モデルを定義します。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model OrderData {
  id          Int @id @default(autoincrement())
  product     String
  price       Int
  quantity    Int
  orderdate   DateTime
}

モデル定義を追記したら、以下のコマンドでマイグレーションを実行してMySQLにテーブルを作成します。

npx prisma migrate dev --name init

実行が完了すると、「OrderData」テーブルが MySQL に作成されます。

Prismaでマイグレーション

また、同時に「generated/prisma」フォルダ配下にPrisma Clientも作成されます。

APIルートの作成

次にAPIルートを作成していきます。「app」フォルダ配下に「api/orderdata/route.ts」を作成し、GETやPOSTのリクエストの処理を記載します。

import { PrismaClient } from '@prisma/client';
import { NextRequest, NextResponse } from 'next/server';

const prisma = new PrismaClient();

export async function GET(req: NextRequest) {
  try {
    const orderData = await prisma.orderData.findMany();
    return NextResponse.json(orderData);
  } catch (error: any) {
    return new NextResponse(
      JSON.stringify({ error: 'Failed to fetch order data', detail: error.message }),
      { 
        status: 500,
        headers: {
          'Content-Type': 'application/json',
        },  
      }
    );
  }
}

export async function POST(req: NextRequest) {
  try {
    const data = await req.json();
    const newOrder = await prisma.orderData.create({
      data: {
        product: data.product,
        quantity: data.quantity,
        price: data.price,
        orderdate: new Date(data.orderdate),
      },
    });
    return NextResponse.json(newOrder);
  } catch (error: any) {
    return new NextResponse(
      JSON.stringify({ error: 'Failed to create order', detail: error.message }),
      { 
        status: 500,
        headers: {
          'Content-Type': 'application/json',
        },  
      }
    );
  }
}

同様に「api/orderdata/[id]/route.ts」を作成し、PUTやDELETE、さらに特定のデータに対するGETリクエストの処理を記載します。

import { PrismaClient } from '@prisma/client';
import { NextRequest, NextResponse } from 'next/server';

const prisma = new PrismaClient();

// GET (特定のデータ)
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  try {
    const { id } = await params;
    const order = await prisma.orderData.findUnique({
      where: {
        id: parseInt(id, 10),
      },
    });
    if (!order) {
      return new NextResponse(
        JSON.stringify({ error: 'Order not found' }),
        { 
          status: 404,
          headers: { 'Content-Type': 'application/json' },  
        }
      );
    }
    return NextResponse.json(order);
  } catch (error: any) {
    return new NextResponse(
      JSON.stringify({ error: 'Failed to fetch order', detail: error.message }),
      { 
        status: 500,
        headers: { 'Content-Type': 'application/json' },  
      }
    );
  }
}

// PUT
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  try {
    const { id } = await params;
    const data = await req.json();
    const updatedOrder = await prisma.orderData.update({
      where: {
        id: parseInt(id, 10),
      },
      data: {
        product: data.product,
        quantity: data.quantity,
        price: data.price,
        orderdate: new Date(data.orderdate),
      },
    });
    return NextResponse.json(updatedOrder);
  } catch (error: any) {
    return new NextResponse(
      JSON.stringify({ error: 'Failed to update order', detail: error.message }),
      { 
        status: 500,
        headers: { 'Content-Type': 'application/json' },  
      }
    );
  }
}

// DELETE
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  try {
    const { id } = await params;
    const deletedOrder = await prisma.orderData.delete({
      where: {
        id: parseInt(id, 10),
      },
    });
    return NextResponse.json(deletedOrder);
  } catch (error: any) {
    return new NextResponse(
      JSON.stringify({ error: 'Failed to delete order', detail: error.message }),
      { 
        status: 500,
        headers: { 'Content-Type': 'application/json' },  
      }
    );
  }
}

作成したら以下のコマンドを実行してAPIを起動します。

npm run dev

curlなどで「http://localhost:3000/api/orderdata」にGETリクエストを送信すると、APIからデータが取得できます(この時点では0件)。

GETリクエスト

Prisma Studioでデータの編集

PrismaはGUIでDBのデータ管理が可能な「Prisma Studio」を提供しています。

APIを起動した状態で、以下のコマンドを実行することでPrisma Studioを起動できます。

npx prisma studio

ブラウザから「http://localhost:5555」にアクセスすると以下のような管理画面でGUIによるデータ編集が可能です。

Prisma Studio

こちらから何件かテストデータを登録しておきます。

フロントエンド部分の作成

以上でバックエンドのWeb APIの準備が整ったので、WijmoのFlexGridでデータ表示を行うフロントエンド部分を作成していきます。

Wijmoのインストールと組み込み

まずはWijmoのReact用パッケージをアプリケーションにインストールします。

npm install @mescius/wijmo.react.all

npmパッケージをインストールしたら、Wijmoの組み込みを行なっていきます。

まずはプロジェクトのルートフォルダに「components」フォルダを作成します。
※ 「components」フォルダがすでに存在する場合はこの手順は不要です。

「components」フォルダ

「components」フォルダに「FlexGrid.tsx」を追加し、以下のように記述します。

WijmoのReactモジュールやCSS、日本語カルチャファイルをインポートし、WijmoのhttpRequestメソッドを使用してWeb APIからデータを取得します。
※ ライセンスキーを設定しない場合トライアル版を示すメッセージが表示されます。ライセンスキーの入手や設定方法についてはこちらをご覧ください。

'use client'

import * as React from "react";
import "@mescius/wijmo.styles/wijmo.css";
import * as WjCore from "@mescius/wijmo";
import * as WjGrid from "@mescius/wijmo.react.grid";
import { FlexGridFilter } from "@mescius/wijmo.react.grid.filter";
import "@mescius/wijmo.cultures/wijmo.culture.ja";
import OrderUpdateButton from "./OrderUpdateButton";

WjCore.setLicenseKey('ここにWijmoのライセンスキーを設定します');

type OrderItem = {
    id: number;
    product: string;
    price: number;
    quantity: number;
    orderdate: Date;
};

const FlexGrid = () => {
    const url = "http://localhost:3000/api/orderdata/";
    const [order, setOrder] = React.useState<WjCore.CollectionView<OrderItem> | null>(null);

    //GET
    React.useEffect(() => {
        WjCore.httpRequest(url, {
            success: (xhr) => {
                setOrder(
                    new WjCore.CollectionView(JSON.parse(xhr.response, reviver), {
                        trackChanges: true,
                        pageSize: 15,
                    })
                );
            },
            error: (xhr) => {
                alert("データの取得に失敗しました");
            }
        });
    }, []);

    const update = async () => {
        if (!order) return;

        // PUT
        const putPromises = order.itemsEdited.map((item: any) =>
            new Promise<void>((resolve, reject) => {
                WjCore.httpRequest(url + item.id, {
                    method: "PUT",
                    data: item,
                    success: () => resolve(),
                    error: () => reject(new Error("PUT Failed")),
                });
            })
        );

        // POST
        const postPromises = order.itemsAdded.map((item: any) =>
            new Promise<void>((resolve, reject) => {
                WjCore.httpRequest(url, {
                    method: "POST",
                    data: item,
                    success: () => resolve(),
                    error: () => reject(new Error("POST Failed")),
                });
            })
        );

        // DELETE
        const deletePromises = order.itemsRemoved.map((item: any) =>
            new Promise<void>((resolve, reject) => {
                WjCore.httpRequest(url + item.id, {
                    method: "DELETE",
                    success: () => resolve(),
                    error: () => reject(new Error("DELETE Failed")),
                });
            })
        );

        try {
            await Promise.all([...putPromises, ...postPromises, ...deletePromises]);
            if (order.itemsEdited.length > 0)
                alert(order.itemsEdited.length + "件のデータを更新しました。");
            if (order.itemsAdded.length > 0)
                alert(order.itemsAdded.length + "件のデータを登録しました。");
            if (order.itemsRemoved.length > 0)
                alert(order.itemsRemoved.length + "件のデータを削除しました。");
            // データ再取得
            WjCore.httpRequest(url, {
                success: (xhr) => {
                    setOrder(
                        new WjCore.CollectionView(JSON.parse(xhr.response, reviver), {
                            trackChanges: true,
                            pageSize: 15,
                        })
                    );
                },
                error: () => {
                    alert("データの再取得に失敗しました");
                }
            });
        } catch (e) {
            alert("一部のリクエストでエラーが発生しました。");
        }
    };

    return (
        <div>
            <div>
                <OrderUpdateButton onClick={update} disabled={!order} />
            </div>
            <div>
                {order ? (
                    <WjGrid.FlexGrid itemsSource={order} allowAddNew={true} allowDelete={true} style={{ width: '680px' }}>
                        <FlexGridFilter />
                        <WjGrid.FlexGridColumn header="ID" binding="id" width={80} />
                        <WjGrid.FlexGridColumn header="製品名" binding="product" width={200} />
                        <WjGrid.FlexGridColumn header="単価" binding="price" width={100} />
                        <WjGrid.FlexGridColumn header="数量" binding="quantity" width={100} />
                        <WjGrid.FlexGridColumn header="受注日" binding="orderdate" width={150} />
                    </WjGrid.FlexGrid>
                ) : (
                    <div>Loading...</div>
                )}
            </div>
        </div>
    );
}

// 日付文字列をDate型に変換するreviver関数
function reviver(key: string, val: any) {
    if (
        typeof val === "string" &&
        /^\d{4}-\d{2}-\d{2}T.*/.test(val)
    ) {
        return new Date(Date.parse(val));
    }
    return val;
}

export default FlexGrid;

さらに「components」フォルダに「OrderUpdateButton.tsx」を追加し、更新処理実行用のボタンのコンポーネントを作成します。

import React from "react";

type Props = {
  onClick: () => void;
  disabled?: boolean;
};

const OrderUpdateButton = ({ onClick, disabled }: Props) => (
  <button type="button" onClick={onClick} disabled={disabled}>
    更新
  </button>
);

export default OrderUpdateButton;

次に「app/page.tsx」ファイルの内容を以下のように書き換えます。「components/FlexGrid.tsx」ファイルを動的インポートし、その際{ ssr: false }オプションを指定することで対象ファイルをSSR(サーバーサイドレンダリング)しないように設定します。

'use client';
import dynamic from 'next/dynamic'

const FlexGrid = dynamic(
  () => {
    return import("../components/FlexGrid");
  },
  { ssr: false }
);

export default function Home() {
  return (
    <>
      <h1 className="mt-2.5 mb-2.5 text-sky-500 text-4xl font-bold">
        Wijmo×Prisma CRUDサンプル
      </h1>
      <FlexGrid></FlexGrid>
    </>
  )
}

また、「app/layout.tsx」のMetadataでタイトルを修正し、加えてlang属性も修正します。

・・・(中略)・・・
export const metadata: Metadata = {
  title: "Wijmo×Prisma CRUDサンプル",
  description: "PrismaとWijmoを使用したCRUDサンプルアプリケーション",
};
・・・(中略)・・・
    <html lang="ja">
・・・(中略)・・・

最後に「app/globals.css」にスタイルを追加します。

@import "tailwindcss";

body{
  margin-left: 10px
}

button {
  @apply bg-blue-500 text-white font-bold py-2 px-4 mb-2 rounded;
}

データの読込(READ)

以上の手順で、Wijmoの組み込みは完了です。再び「npm run dev」コマンドを実行して「http://localhost:3000/」にブラウザでアクセスすると、FlexGrid上にAPIから取得したデータが表示されていることを確認できます。

データの読込(READ)

今回のサンプルでは、FlexGridFilterコンポーネントも組み込んでいるため(FlexGrid.tsxファイル115行目)、Excelライクなソートやフィルタも利用可能です。

データの登録(CREATE)

FlexGridの一番下の行にデータを入力することで新規データの登録が可能です。データ入力後[更新]ボタンを押下するとAPIにPOSTのリクエストを送信できます。

データの更新(UPDATE)

FlexGrid上で任意のデータを更新し、[更新]ボタンを押下するとAPIにPUTのリクエストを送信できます。一度に複数件の更新も可能です。

データの削除(DELETE)

FlexGrid上で削除したい行を選択しDeleteキーを押下すると対象の行を削除できます。その後、[更新]ボタンを押下するとAPIにDELETEのリクエストを送信できます。

今回作成したアプリケーションは以下よりダウンロード可能です。

さいごに

今回はPrismaとNext.jsを使用してMySQLと連携するWeb APIを作成し、Wijmoのデータグリッド「FlexGrid」でCRUD処理を行うWebアプリケーションを作成する方法をご紹介しました。

製品サイトでは、Wijmoの機能を手軽に体験できるデモアプリケーションやトライアル版も公開しておりますので、こちらもご確認ください。

また、ご導入前の製品に関するご相談、ご導入後の各種サービスに関するご質問など、お気軽にお問合せください。

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