SvelteKitで社内AIツールをフルスタック構築した所感 — SSE・Playwright・暗号化を1フレームワークに収める

SvelteKitで社内AIツールをフルスタック構築した所感 — SSE・Playwright・暗号化を1フレームワークに収める

SvelteKitをNode adapterでデプロイし、UI・API・SSEストリーミング・Playwright認証・SQLiteデータベース・国際化を1プロジェクトに統合して社内AIツールを構築した実体験をまとめます。
2026.07.30

はじめに

社内向けのAIプロンプト実行ツールを構築する際、フレームワークにSvelteKitを選びました。フロントエンドのUIだけでなく、サーバーサイドのAPIルート、Playwrightによるブラウザ認証、SSEストリーミング、SQLiteデータベースまで——すべてを1つのSvelteKitプロジェクトに収めています。

社内ツールという文脈で、SvelteKitをフルスタックフレームワークとして使った実体験をまとめます。

前提・環境

  • SvelteKit v2 + Svelte 5
  • @sveltejs/adapter-node(Nodeサーバーとしてデプロイ)
  • Drizzle ORM(SQLite)
  • Playwright(サーバーサイド認証)
  • Paraglide.js(i18n)
  • TailwindCSS v4 + shadcn-svelte

アーキテクチャ全体像

src/
├── routes/
│   ├── +page.svelte          ← メインUI
│   ├── +layout.svelte         ← アプリシェル
│   └── api/
│       ├── auth/              ← 認証API
│       │   ├── login/
│       │   ├── logout/
│       │   └── status/
│       └── gpt/               ← AI API
│           ├── generate/      ← SSEストリーミング
│           ├── thread/
│           ├── chat/
│           └── assistant/
├── lib/
│   ├── client/                ← ブラウザ側コード
│   │   ├── gptService.ts
│   │   ├── authService.ts
│   │   └── credentialStorage.ts
│   ├── server/                ← サーバー側コード
│   │   ├── auth/              ← Playwright認証
│   │   ├── services/          ← AI APIラッパー
│   │   └── db/                ← Drizzle ORM
│   ├── components/            ← UIコンポーネント
│   ├── templates/             ← テンプレートJSON
│   └── paraglide/             ← i18nランタイム(自動生成)
└── hooks.server.ts            ← Paraglideミドルウェア

SvelteKitの特徴

APIルートとUIが同じプロジェクトに共存する

src/routes/api/gpt/generate/+server.ts
import aiService from "$lib/server/services/aiService";

export const POST: RequestHandler = async ({ request }) => {
  const { prompt, name, instruction, webSearch } = await request.json();
  const response = await aiService.chat(name, instruction, prompt, !!webSearch);

  return new Response(response.data, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive"
    }
  });
};

$lib/server/にあるコードはサーバーサイドでのみ実行されます。SvelteKitがビルド時にこの分離を保証するため、「うっかりサーバーのコードがクライアントにバンドルされる」事故が起きません。

SSEストリーミングが標準のWeb APIで動く

SvelteKitのAPIルートはWeb標準のResponseオブジェクトを返します。SSEストリーミングを返すには、ストリームをResponseに渡すだけです。特別なライブラリやミドルウェアは不要でした。

Svelte 5のrunesによるリアクティブ状態管理

+page.svelte
let output = $state('');
let isLoading = $state(false);
let isStreaming = $state(false);
let selectedTemplate = $state<Template | null>(null);
let isParameterInputCollapsed = $state(false);

Svelte 5の$stateルーンは、ReactのuseStateに相当しますが、セッターメソッドなしで直接代入できます。ストリーミング中のUI状態管理(ロード中、ストリーミング中、パラメータ折りたたみ状態など)が自然に書けます。

hooks.server.tsでミドルウェアを挟む

hooks.server.ts
import { paraglideMiddleware } from '$lib/paraglide/server';

const handleParaglide: Handle = ({ event, resolve }) =>
  paraglideMiddleware(event.request, ({ request, locale }) => {
    event.request = request;
    return resolve(event, {
      transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
    });
  });

export const handle: Handle = handleParaglide;

すべてのリクエストを通過するミドルウェアをhooks.server.tsに定義できます。ここではParaglideのロケール検出を行い、HTMLのlang属性を動的に設定しています。

実際に感じた課題

1ファイルが大きくなりがち

メインページの+page.svelteが1198行になりました。SvelteKitのファイルベースルーティングでは、ページごとに1つの.svelteファイルが対応するため、ページ内のロジックが増えるとファイルが肥大化します。

対策としてコンポーネント分割(AuthSettings.svelte, TemplateManager.svelte, OutputDisplay.svelteなど)は行いましたが、ページレベルの状態管理は+page.svelteに残ります。

コンポーネントライブラリのCSS scoping問題

shadcn-svelteのようなコンポーネントライブラリを使うと、SvelteのCSS scopingと衝突することがあります。

<!-- ライブラリのButtonコンポーネントに独自クラスを適用 -->
<Button class="locale-switcher-btn">...</Button>

<style>
  /* NG: スコープされてライブラリのDOMに届かない */
  .locale-switcher-btn { background: rgba(255, 255, 255, 0.05); }

  /* OK: :global()でスコープを外す */
  :global(.locale-switcher-btn) { background: rgba(255, 255, 255, 0.05); }
</style>

:global()ラッパーが必要なケースが度々発生しました。

コンパイルタイムi18nの利点と制約

Paraglide.jsはビルド時にメッセージファイルからTypeScriptコードを生成します。

import * as m from '$lib/paraglide/messages/_index.js';

// 型安全、自動補完が効く
error = m.please_fill_required({ fields: missingRequired.join(', ') });

ランタイムi18nライブラリ(i18next等)と比べて:

項目 Paraglide(コンパイルタイム) i18next(ランタイム)
型安全性 ○ 引数の型チェック × 文字列キー
バンドルサイズ ○ 使うロケールのみ △ 全ロケール読み込み
柔軟性 △ ビルドが必要 ○ 動的にロケール追加可能
サーバーサイド ○ ミドルウェアで対応

社内ツールで対応言語が限定されている(英語・日本語のみ)場合、コンパイルタイムi18nの型安全性は大きな利点です。

SvelteKitが1プロジェクトで担当している範囲

このプロジェクトでSvelteKitが担っている役割をまとめると:

役割 技術要素
フロントエンドUI Svelte 5 + shadcn-svelte + TailwindCSS
APIサーバー SvelteKit server routes
SSEストリーミング Web Response API
認証プロキシ Playwright(サーバーサイド)
データベース Drizzle ORM + SQLite
国際化 Paraglide.js(コンパイルタイム)
Markdownレンダリング mdsvex
デプロイ adapter-node → Nodeサーバー

従来であれば「Reactフロントエンド + Expressバックエンド」のように2つのプロジェクトに分けるところを、1つのSvelteKitプロジェクトに統合しています。

まとめ

SvelteKitをフルスタックフレームワークとして社内ツールに使った結果:

  • サーバーサイドコードの分離が明確で、Playwrightのような重い依存もクリーンに管理できた
  • SSEストリーミングがWeb標準APIで自然に書けた
  • Svelte 5のrunesで状態管理がシンプルになった
  • コンパイルタイムi18nの型安全性は小規模な多言語対応に適していた
  • ページファイルの肥大化とCSS scopingの問題は注意が必要

社内ツールは「少人数で素早く作り、自分たちで運用する」ケースが多いです。フロントエンドとバックエンドを1プロジェクトで管理できるSvelteKitは、この用途に合っていると感じました。

この記事をシェアする

関連記事