
SvelteKitで社内AIツールをフルスタック構築した所感 — SSE・Playwright・暗号化を1フレームワークに収める
はじめに
社内向けの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が同じプロジェクトに共存する
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によるリアクティブ状態管理
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でミドルウェアを挟む
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は、この用途に合っていると感じました。







