この記事はRust+SvelteKit+CDK で RSS 要約アプリを作ってみる Advent Calendar 2025の 19 日目の記事になります。
また、筆者が属している株式会社野村総合研究所のアドベントカレンダーもあるので、ぜひ購読ください。
はじめに
フレームワークごとに前提としたディレクトリ構成が存在しますが、トップレベルのディレクトリ構成を定めているくらいで、サブディレクトリレベルの構成設計は開発者に委ねられていることが多いです。
こういったとき、ある程度の「ディレクトリ構成の型」を決めておかないと、保守性や可読性が低下する可能性があります。
筆者もまだ正解を見つけられてはないのですが、今回の Web アプリ開発や過去の経験から、採用した・これから採用しようと思っている構成を紹介します。
SvelteKit のディレクトリ構成
1. src/routes
ここには「ルーティングに関わるファイル(+page.svelte, +layout.svelte, +page.tsなど)」のみ を置くのが推奨されます。
ここのディレクトリ構成がそのままブラウザパスになります。規定のファイル名以外は無視されますが、そもそも役割ごとにファイルを分けることが SvelteKit の標準であるので、可能な限りルーティング以外のファイルを置かないようにします。
2. src/lib
ルーティング関連以外のアプリケーションの核となるコード(コンポーネント、ユーティリティ、型定義など)はすべてここに置きます。
SvelteKit は、このディレクトリを$libエイリアスを使ってどこからでもインポートできる仕組みになっています。
SumaRSS での実装
ディレクトリ構成
以下のような構成を採用しました。
src/
├── lib/
│ ├── components/
│ │ ├── ui/ # shadcn/uiなどの汎用UI部品(Atomic)
│ │ │ ├── button.svelte
│ │ │ └── card.svelte
│ │ ├── ArticleCard.svelte # ドメイン固有のコンポーネント
│ │ └── Header.svelte
│ ├── utils.ts # 汎用ユーティリティ
│ └── types.ts # 型定義
└── routes/
├── +layout.svelte # 全体レイアウト
├── +page.svelte # トップページ
└── blog/
└── [date]/ # 日付別ページ
└── +page.svelte
コンポーネント設計のルール
UI コンポーネント (lib/components/ui)
ボタン、カード、バッジなど、「ドメイン知識を持たない」 純粋な UI 部品です。
これらは再利用性が高く、他のプロジェクトでも使い回せるように設計します(今回は shadcn/ui をベースにしています)。shadcn/ui の仕様として、このディレクトリに自動生成することになっていますが、shadcn/ui に頼らない自作の UI コンポーネントもこのディレクトリに置くようにしています。
ドメインによらない汎用部品をuiディレクトリのような場所に置くことは業務でもよく採用します。ディレクトリを分けることで、そのディレクトリの中に作成するコンポーネントの責務(汎用的かつスタイリングの統一を担う)を意識しやすいからです。
ドメインコンポーネント (lib/components)
ArticleCard.svelteのように、「特定のデータ構造やビジネスロジックに依存する」 コンポーネントです。
今回はlib/componentsの下にフラットに置いていますが、より階層構造を意識して分けることも可能です。ドメインの数が多く、フラットに配置すると見通しが悪くなる場合は、サブディレクトリを分けたほうがいいと思います。
サブディレクトリの分け方ですが、lib/articlesのように、まず「機能」でサブディレクトリを作成し、その下にlib/articles/componentsといったコンポーネントのディレクトリを作成することを推奨します。componentsやtypesといった「技術要素」で先にサブディレクトリを切るよりも、ドメインや機能といった観点でまとめることで、変更箇所をより限定することが可能です。例えばusersというサブディレクトリの下にはusersドメインが使うコンポーネント・型定義・ユーティリティなどがまとまるので、ユーザ情報を変更したり画面の表示項目と API の IF を変更したいときにはusersディレクトリ内部の変更のみで完結します。このような考え方を co-location(コロケーション)と呼び、筆者は実際の業務でもこのディレクトリ構成を採用しています。
今回の Web アプリはそこまでドメインや機能の数が多くないので co-location とはしていません。
ArticleCard.svelteはsrc/lib/types.tsで定義された型を受け取り、ビジネスロジック(表示ロジック)を含みます。
<!-- ArticleCard.svelte -->
<script lang="ts">
import type { Article } from '$lib/types';
import { Card } from '$lib/components/ui/card';
// Svelte 5のRunes記法で型定義
let { article }: { article: Article } = $props();
</script>
<Card>
<!-- ... -->
</Card>
型定義の集約
src/lib/types.tsにアプリケーション全体で使う型を集約しています。
export type Article = {
link: string;
title: string;
date: string;
summary_short: string;
summary_long: string;
source: string;
id: string;
state: "Pending" | "Summarized";
feed_name: string;
};
これにより、フロントエンド全体で「Article」といえばこの形、という共通認識が生まれます。
SvelteKity より外側(バックエンド)のアプリと IF を合わせる場合は、SSOT(Single Source of Truth)をどこに配置するかが重要になります。
今回は小規模であるため、Rust で書いた Lambda と TS で書いた SvelteKit とで型定義を別々に書きました(=二重管理)。小規模であればこれでもほぼ問題はありませんが、大規模になるのであればどちらかを SSOT として、もう片方はそれに従属する形で自動生成するのが望ましいです。
よく採用されるのは OpenAPI スキーマです。REST API を意識した仕様ではありますが、リソースの型も定義できます。他にも tRPC、または gRPC のような仕様も考慮できます。
今の時代、AI によって統一させる、という方法もあるんでしょうか。。。それだと言語やフレームワークを問わないのですごい便利かもしれません。