カテゴリー
 Angular/SSRアプリケーションでPrisma/SQLiteを利用する際の注意点  
※ 当ページには【広告/PR】を含む場合があります。
2025/07/03

Angularに限らず、React/Vueのような他のフロントエンドフレームワークであっても、データベース用のサーバーを別に建てずに、DEVサーバーからローカルに配置したSQLiteデータベースに直接アクセスして利用できたら...と思うことがしばしばあるかもしれません。
アイデアとしては一見難しくなさそうに感じますが、いくつかの"難所"で立ち止まることになるかもしれません。
これは、通常、フロントエンドフレームワークが扱えるのはブラウザ(Javascript)のロジックであり、対してSQLiteを含むデータベースの操作はサーバー(Nodejs)のロジックのため、水と油の関係のように中々混じり合わせるのが困難であるためです。
今回はAngularでPrismaクライアントをどう組み込めば良いかを考えてみます。
SSRの仕組みを使う
これはAngularに限らず他の主要なフロントエンドフレームワークでも何かしらサポートされる機能になりましたが、SSR(サーバーサイドレンダリング)の仕組みを利用する必要があります。
SSRベースのアプリケーションを作成したい場合、Angularでは
「@angular/ssr」ここらへんの話は以前特集した記事にて詳しく取り上げていました。
Angular/SSRのプロジェクトの導入・移行手順はそちらを参考にしてください。
ここでは本題だけに集中して議論していきましょう。
Angular/SSRにて、サーバーのロジックを提供しているのは、「server.ts」です。
少し個人用に初期のserver.tsからファイル分離したりと、カスタマイズしていますが、おおよそ現状は以下のような実装になっています。
            import { createNodeRequestHandler } from '@angular/ssr/node';
import express from 'express';
import asyncify from 'express-asyncify';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { devRoute } from './dev';
export function app(): express.Express {
    const server = asyncify(express());
    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');
    server.use(devRoute({
        distFolderRaw: browserDistFolder
    }));
    return server;
}
export const server = app();
export const reqHandler = createNodeRequestHandler(server);
        こちらは分離したルートの定義です。
            import { Router, static as static_ } from 'express';
import asyncify from 'express-asyncify';
import { writeResponseToNodeResponse, createWebRequestFromNodeRequest } from '@angular/ssr/node';
import { AngularAppEngine } from '@angular/ssr';
export const devRoute = ({ distFolderRaw }: { distFolderRaw: string }) => {
    const devRouter = asyncify(Router({ mergeParams: true }));
    const angularAppEngine = new AngularAppEngine();
    // All regular routes use the Angular engine
    devRouter.get(/(.*)/, async (req, res, next) => {
        const _req = createWebRequestFromNodeRequest(req);
        const _res = await angularAppEngine.handle(_req);
        if (_res) {
            if (req.url === '/error') {
                const _resp = new Response(_res.body, {
                    status: 404,
                    headers: _res.headers
                });
                writeResponseToNodeResponse(_resp, res);
            }
            else {
                writeResponseToNodeResponse(_res, res);
            }
        } else {
            next()
        }
    });
    return devRouter;
};
        いわゆるサーバーはExpress.jsの実装そのものです。
では、サーバーのロジックが入れられる場所が分かりましたので、おもむろにここで
/api            import { Router, static as static_ } from 'express';
import asyncify from 'express-asyncify';
import { writeResponseToNodeResponse, createWebRequestFromNodeRequest } from '@angular/ssr/node';
import { AngularAppEngine } from '@angular/ssr';
//👇Prismaクライアントを追加
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const devRouexport const devRoute = ({ distFolderRaw }: { distFolderRaw: string }) => {
    const devRouter = asyncify(Router({ mergeParams: true }));
    const angularAppEngine = new AngularAppEngine();
    //👇/apiルートにアクセスしたときに適当なデータを取得できる?
    devRouter.get('/api', async (req,res) => {
        const price = await prisma.priceInfo.findUnique({
            where: { id: 4 }
        });
        console.log(price);
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(foundUser, null, 3));
    });
    // All regular routes use the Angular engine
    devRouter.get(/(.*)/, async (req, res, next) => {
        const _req = createWebRequestFromNodeRequest(req);
        const _res = await angularAppEngine.handle(_req);
        if (_res) {
            if (req.url === '/error') {
                const _resp = new Response(_res.body, {
                    status: 404,
                    headers: _res.headers
                });
                writeResponseToNodeResponse(_resp, res);
            }
            else {
                writeResponseToNodeResponse(_res, res);
            }
        } else {
            next()
        }
    });
    return devRouter;
};
        で、これで適当なhttpClientを仕込んでブラウザから
/apiここでは詳しい実装は紹介しませんが、以下のようなサービスを使うと良いでしょう。
            //...
private http = inject(HttpClient);
//...
this.http.get(`api`, { responseType: 'json' })
     .pipe(catchError((err: any) => {return EMPTY;}));
        サーバー起動し、サービスのメソッドを呼び出すと、時に以下のようなエラーが起こります。
            #...
5:01:10 PM [vite] (ssr) Error when evaluating SSR module ./server.mjs: __dirname is not defined
#...
        このエラーは本来サーバーの中だけで動いてほしい処理が、ブラウザ内部まで処理をしてしまったため、ブラウザには存在しないはずのNodejsの変数
__dirnameこれを解決するためには、
angular.json[architect] > [build] > [options]externalDependencies            //...
"architect": {
  "build": {
    "builder": "@angular/build:application",
    "options": {
      //👇を追加
            "externalDependencies": [
              "@prisma/client"
            ],
//...
        これで今回で言えば、
@prisma/clientこれで、サーバー側でPrismaクライアントがSQLiteを操作し、
              ➜  Local:   http://localhost:1234/
  ➜  Network: http://172.19.0.2:1234/
#...
{
  id: 4,
  datetime: 2025-06-29T12:58:16.623Z,
  boardInfo: '{"price":1000}',
  productCodeId: '7548'
}
        そこからSSRの仕組みを通して、ブラウザへデータを送り出してくれるようになります。

externalDependenciesの落とし穴
もう一点、externalDependenciesへの設定にも注意が必要です。
例えば、Prismaでは、Prismaクライアントのリソースをデフォルトの
@prisma/client            generator client {
  provider = "prisma-client-js"
  //出力先を@prisma/client外に指定
  output   = "../build"
}
//...
        として、
prisma generatebuildそこから先程と同様に、
angular.json            //...
"architect": {
  "build": {
    "builder": "@angular/build:application",
    "options": {
            "externalDependencies": [
              //👇試しに相対フォルダパスを指定
              "./build"
            ],
//...
        ビルドすると以下のようなエラーが起こります。
            5:20:21 PM [vite] (ssr) Error when evaluating SSR module ./server.mjs: EISDIR: illegal operation on a directory, read build
        この場合、相対パス指定だとビルドシステムがどこを読んでいいのか分からないようです。
そこで、tsconfig.jsonでパスエイリアス(paths)を設定して、ビルドシステムに改めてモジュールの場所を設定してみましょう。
            //...
"compilerOptions": {
    //...
    //👇ディレクトリのエイリアスを設定
    "paths": {
      "prisma_client": [
        "./build"
      ],
    }
        このモジュール名で、
angular.json            //...
"externalDependencies": [
   "prisma_client"
],
//...
        再度ビルドすると...
            5:24:26 PM [vite] (ssr) Error when evaluating SSR module ./server.mjs: Cannot find module 'prisma_client' imported from '/usr/src/app/.angular/vite-root/kubochart-viewer/server.mjs'
        結果はフォルダにエイリアスを張っても同じことで、
node_modules結果として、
externalDependenciesnode_modulesyarn link(npm link)を使う
とはいえ、どうしても別プロジェクトで開発中のライブラリをお試しパッケージとして、現在のプロジェクトに呼び込みたいときなど、やっぱり使えないと困るケースがあります。
そんな場合、
「yarn link(もしくはnpm link)」この考え方は、
prisma generateでは先程のPrismaスキーマを使って確認してみましょう。
            generator client {
  provider = "prisma-client-js"
  //出力先を@prisma/client外に指定
  output   = "../build"
}
//...
        これをベースに現在のPrismaスキーマファイルから見て
../build            $ prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
✔ Generated Prisma Client (v6.10.1) to ./build in 222ms
$ cd build/
$ ls
client.d.ts  default.d.ts  edge.d.ts  index-browser.js  index.js                                      package.json  schema.prisma  wasm.js
client.js    default.js    edge.js    index.d.ts        libquery_engine-debian-openssl-3.0.x.so.node  runtime       wasm.d.ts
        ちゃんと
buildでは、ここにパッケージのシンボリックリンクを付与しましょう。
            $ yarn link
yarn link v1.22.22
success Registered "prisma-client-9c30d2139563302f94cb830531764488cccf72fda868b2d7c729e640016f7629".
info You can now run `yarn link "prisma-client-9c30d2139563302f94cb830531764488cccf72fda868b2d7c729e640016f7629"` in the projects where you want to use this package and it will be used instead.
        これでこのPrismaクライアントがシンボリックリンクを通じて別のnodejsプロジェクトから呼び出せる準備ができました。
注意すべきは、シンボリックリンクで生成されるパッケージの名前は、
package.jsonnameなので外部に生成したPrismaクライアントは
prisma-client-<ハッシュID>そのまま使うと混乱してしまいそうなので、使う際にはtsconfig.jsonにエイリアスを設定しましょう。
            //...
"paths": {
      "prisma_client": [
        "prisma-client-9c30d2139563302f94cb830531764488cccf72fda868b2d7c729e640016f7629"
      ]
    }
//...
        では元のプロジェクトに戻り、作成したシンボリックリンクを呼び込みます。
            $ yarn link prisma-client-9c30d2139563302f94cb830531764488cccf72fda868b2d7c729e640016f7629
yarn link v1.22.22
success Using linked package for "prisma-client-9c30d2139563302f94cb830531764488cccf72fda868b2d7c729e640016f7629".
        もちろん
angular.json            //...
"externalDependencies": [
   "prisma_client"
],
//...
        これで問題なく動作するはずです。
なお、Generateするたびに
prisma-client-<ハッシュID>よってパッケージ名が変わるたびにシンボリックリンクは再設定しないこといけないことをお忘れなく。
まとめ
今回はAngular/SSRとPrismaクライアントを組み合わせて、ローカルなSQLiteを操作する設定手段を紹介してみました。
おそらくはPrismaクライアントに限らず、ブラウザとサーバーのロジックを分離したいユースケースにも今回のテクニックが役に立つと思います。