Angular/SSRアプリケーションでPrisma/SQLiteを利用する際の注意点


※ 当ページには【広告/PR】を含む場合があります。
2025/07/03
Viteからangularプロジェクトをサクッと始めてみる
Angular/SSRでブラウザにあってNodejsにないJavascript APIクラスを使う際に気を使うこと
蛸壺の技術ブログ|Angular/SSRアプリケーションでPrisma/SQLiteを利用する


Angularに限らず、React/Vueのような他のフロントエンドフレームワークであっても、データベース用のサーバーを別に建てずに、DEVサーバーからローカルに配置したSQLiteデータベースに直接アクセスして利用できたら...と思うことがしばしばあるかもしれません。
アイデアとしては一見難しくなさそうに感じますが、いくつかの"難所"で立ち止まることになるかもしれません。
これは、通常、フロントエンドフレームワークが扱えるのはブラウザ(Javascript)のロジックであり、対してSQLiteを含むデータベースの操作はサーバー(Nodejs)のロジックのため、水と油の関係のように中々混じり合わせるのが困難であるためです。

今回はAngularでPrismaクライアントをどう組み込めば良いかを考えてみます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

SSRの仕組みを使う



これはAngularに限らず他の主要なフロントエンドフレームワークでも何かしらサポートされる機能になりましたが、SSR(サーバーサイドレンダリング)の仕組みを利用する必要があります。
SSRベースのアプリケーションを作成したい場合、Angularでは
「@angular/ssr」 を利用することになります。
ここらへんの話は以前特集した記事にて詳しく取り上げていました。

合同会社タコスキングダム|蛸壺の技術ブログ
【Angularの新しいSSR環境】「Angular Universal」から「@angular/ssr」へのマイグレーションガイド

Angular Universalを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 というルートを定義し、Prismaクライアント経由でSQLiteのデータへアクセスできるか確かめてみましょう。

            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 がサーバー(Nodejs)のロジックとすることができて、ブラウザ側からのソースコードの読み込みを抑制することが可能です。
これで、サーバー側で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の仕組みを通して、ブラウザへデータを送り出してくれるようになります。

合同会社タコスキングダム|蛸壺の技術ブログ

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

externalDependenciesの落とし穴



もう一点、externalDependenciesへの設定にも注意が必要です。
例えば、Prismaでは、Prismaクライアントのリソースをデフォルトの
@prisma/client 以外にも外部のモジュールとして任意のフォルダに出力することが可能です。

            generator client {
  provider = "prisma-client-js"
  //出力先を@prisma/client外に指定
  output   = "../build"
}

//...

        

として、
prisma generate コマンドを実行した場合、prismaスキーマファイルのあるフォルダ位置からひとつ上の build へPrismaクライアントが展開されることになります。
そこから先程と同様に、
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 内にソースコードの実体がなければ、Nodejsが正しく動作してくれないようです。
結果として、
externalDependencies に正しく登録できるのは、基本的に現在のプロジェクトの node_modules 以下に配置されたモジュールだけと考えておくと良いでしょう。

yarn link(npm link)を使う



とはいえ、どうしても別プロジェクトで開発中のライブラリをお試しパッケージとして、現在のプロジェクトに呼び込みたいときなど、やっぱり使えないと困るケースがあります。
そんな場合、
「yarn link(もしくはnpm link)」 コマンドを使うことで、開発中のパッケージフォルダへシンボリックリンクを張ることが可能です。
この考え方は、
prisma generate コマンドで生成したPrismaクライアントにも適用することができます。

では先程の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 フォルダにPrismaクライアントのパッケージが出力されていることが確認できました。
では、ここにパッケージのシンボリックリンクを付与しましょう。

            $ 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> のパッケージ名が変更になります。
よってパッケージ名が変わるたびにシンボリックリンクは再設定しないこといけないことをお忘れなく。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

まとめ



今回はAngular/SSRとPrismaクライアントを組み合わせて、ローカルなSQLiteを操作する設定手段を紹介してみました。
おそらくはPrismaクライアントに限らず、ブラウザとサーバーのロジックを分離したいユースケースにも今回のテクニックが役に立つと思います。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集