このガイドは、Bun・TypeScript・SvelteKit・Prisma・Auth.js・Tailwind CSS を使ってシンプルなアカウント管理機能付きのWebアプリを作る方法を、高校生でもわかるように説明しています。最新バージョン(2025年11月時点)の情報を使用しています。
各ツールの役割:
- Bun … JavaScriptを実行するための高速ランタイム(Node.jsの置き換え)
- TypeScript … 型を付けたJavaScript(間違いを減らせる)
- SvelteKit … Webサイトを作るフレームワーク
- Prisma … データベースを簡単に操作するORM
- Auth.js … ユーザーのログイン・ログアウトを管理
- Tailwind CSS … CSSを簡単に書くためのツール
第1章:プロジェクトの作成と初期セットアップ
1-1. プロジェクトの作成
まず、ターミナル(コマンドプロンプト)で以下を実行します:
bunx sv create my-app対話形式で質問されるので、以下のように選択してください:
◇ Which template would you like?│ SvelteKit minimal
◇ Add type checking with Typescript?│ Yes, using Typescript syntax
◇ What would you like to add to your project?│ tailwind, prettier, eslint
◇ Which package manager do you want to install dependencies with?│ bun1-2. プロジェクトフォルダへ移動
cd my-app1-3. Bun開発サーバーの起動確認
bun run devブラウザで http://localhost:5173 を開いて、SvelteKitのデモページが表示されたら成功です。
第2章:データベースのセットアップ
2-1. PostgreSQL データベースの準備
開発用には SQLite を使う方法もありますが、本ガイドではシンプルな SQLite を推奨します。
Prismaはファイルベースの SQLite をデフォルトで使えるため、複雑な設定は不要です。
2-2. Prisma のインストール
bun add @prisma/clientbun add -D prisma2-3. Prisma の初期化
bunx prisma initこのコマンドにより、以下のファイルが作成されます:
prisma/schema.prisma… データベースの構造を定義.env… 環境変数(パスワードなど秘密の情報)
2-4. .env ファイルの設定
.env ファイルを編集して、データベース接続文字列を設定します:
DATABASE_URL="file:./dev.db"SQLite をファイルベースで使う場合は、この設定で十分です。
2-5. Prisma スキーマの作成
prisma/schema.prisma を以下のように編集します:
generator client { provider = "prisma-client-js"}
datasource db { provider = "sqlite" url = env("DATABASE_URL")}
model User { id String @id @default(cuid()) email String @unique name String? password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}
model Session { id String @id @default(cuid()) userId String expiresAt DateTime createdAt DateTime @default(now())}
model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refreshToken String? accessToken String? expiresAt Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([provider, providerAccountId])}説明:
User… ユーザー情報(メール、名前、パスワード)Session… ログイン状態の管理Account… OAuth(Google、GitHubなど)の情報を保存
2-6. Prisma 初期マイグレーション
bunx prisma migrate dev --name initこのコマンドにより、SQLite ファイルが作成され、テーブルが生成されます。
第3章:Auth.js のセットアップ
3-1. 必要なパッケージをインストール
bun add @auth/sveltekit @auth/prisma-adapterbun add -D @types/node3-2. AUTH_SECRET の生成
認証に必要なシークレットキーを生成します:
# UNIX系(Mac、Linux)openssl rand -hex 32Windows の場合: https://generate-secret.vercel.app/32 にアクセスして生成してください。
生成されたキーを .env に追加します:
DATABASE_URL="file:./dev.db"AUTH_SECRET="ここに生成されたキーをコピー"AUTH_TRUST_HOST="true"3-3. Auth.js の設定ファイルを作成
src/auth.ts を新規作成して、以下のコードを記述します:
import { SvelteKitAuth } from "@auth/sveltekit";import { PrismaAdapter } from "@auth/prisma-adapter";import Credentials from "@auth/sveltekit/providers/credentials";import { prisma } from "$lib/server/db";import { compare } from "bcrypt";import type { SvelteKitAuthConfig } from "@auth/sveltekit";
async function authorize(credentials: any) { if (!credentials?.email || !credentials?.password) { throw new Error("メールとパスワードを入力してください"); }
const user = await prisma.user.findUnique({ where: { email: credentials.email }, });
if (!user) { throw new Error("ユーザーが見つかりません"); }
const isPasswordValid = await compare(credentials.password, user.password);
if (!isPasswordValid) { throw new Error("パスワードが間違っています"); }
return { id: user.id, email: user.email, name: user.name, };}
export const { handle, signIn, signOut } = SvelteKitAuth(async (event) => { const authOptions: SvelteKitAuthConfig = { adapter: PrismaAdapter(prisma), providers: [ Credentials({ credentials: { email: { label: "メールアドレス" }, password: { label: "パスワード", type: "password" }, }, authorize, }), ], pages: { signIn: "/login", }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; } return token; }, async session({ session, token }) { if (session.user) { session.user.id = token.id as string; } return session; }, }, };
return authOptions;});3-4. サーバーフック(ミドルウェア)の設定
src/hooks.server.ts を新規作成して、以下のコードを記述します:
export { handle } from "./auth";3-5. Prisma クライアントの初期化
src/lib/server/db.ts を新規作成して、以下のコードを記述します:
import { PrismaClient } from "@prisma/client";
const prismaClientSingleton = () => { return new PrismaClient();};
type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;
const globalForPrisma = globalThis as unknown as { prisma: PrismaClientSingleton | undefined;};
export const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;3-6. セッション型定義
src/app.d.ts を編集して、以下を追加します:
import type { DefaultSession } from "@auth/sveltekit";
declare module "@auth/sveltekit" { interface Session { user?: DefaultSession["user"] & { id: string; }; }}
declare global { namespace App { interface Locals { auth: import("@auth/sveltekit").Auth; } }}
export {};第4章:Tailwind CSS の設定
4-1. Tailwind は既にインストール済み
プロジェクト作成時に Tailwind を選択したので、必要なパッケージは既にインストールされています。
4-2. vite.config.ts の確認
vite.config.ts が以下のようになっているか確認してください:
import { sveltekit } from "@sveltejs/kit/vite";import { defineConfig } from "vite";import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ plugins: [sveltekit(), tailwindcss()],});4-3. src/app.css の設定
src/app.css ファイルを作成・編集して、以下を記述します:
@import "tailwindcss";Tailwind v4 では @import "tailwindcss" だけで全ての Tailwind スタイルが読み込まれます。
4-4. src/routes/+layout.svelte で CSS をインポート
src/routes/+layout.svelte を作成・編集して、以下を記述します:
<script> import "../app.css"</script>
<slot />第5章:パスワード暗号化(bcrypt)のセットアップ
ユーザーのパスワードは平文で保存してはいけません。必ず暗号化します。
5-1. bcrypt パッケージをインストール
bun add bcryptbun add -D @types/bcrypt5-2. パスワード暗号化・検証ヘルパー関数
src/lib/server/password.ts を新規作成して、以下を記述します:
import { hash, compare } from "bcrypt";
const SALT_ROUNDS = 10;
export async function hashPassword(password: string): Promise<string> { return await hash(password, SALT_ROUNDS);}
export async function verifyPassword( password: string, hashedPassword: string,): Promise<boolean> { return await compare(password, hashedPassword);}第6章:ページとルートの作成
6-1. ログインページ
src/routes/login/+page.svelte を新規作成して、以下を記述します:
<script lang="ts"> import { signIn } from "../../auth" import type { ActionData } from "./$types"
export let form: ActionData
let email = "" let password = ""</script>
<div class="flex items-center justify-center min-h-screen bg-gray-100"> <div class="bg-white p-8 rounded shadow-md w-96"> <h1 class="text-2xl font-bold mb-6">ログイン</h1>
<form method="POST" action="?/login"> <div class="mb-4"> <label for="email" class="block text-sm font-medium"> メールアドレス </label> <input type="email" name="email" id="email" bind:value={email} class="mt-1 w-full px-3 py-2 border border-gray-300 rounded" required /> </div>
<div class="mb-4"> <label for="password" class="block text-sm font-medium"> パスワード </label> <input type="password" name="password" id="password" bind:value={password} class="mt-1 w-full px-3 py-2 border border-gray-300 rounded" required /> </div>
{#if form?.error} <div class="mb-4 text-red-600 text-sm">{form.error}</div> {/if}
<button type="submit" class="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600" > ログイン </button> </form>
<p class="mt-4 text-sm text-center"> アカウントをお持ちでないですか? <a href="/register" class="text-blue-500 hover:underline"> サインアップ </a> </p> </div></div>6-2. ログインアクション
src/routes/login/+page.server.ts を新規作成して、以下を記述します:
import { signIn } from "../../auth";import { redirect } from "@sveltejs/kit";import type { Actions } from "./$types";
export const actions: Actions = { login: async (event) => { try { await signIn("credentials", { email: event.request.formData().then((d) => d.get("email")), password: event.request.formData().then((d) => d.get("password")), redirect: false, });
redirect(303, "/dashboard"); } catch (error) { return { error: "ログインに失敗しました。メールアドレスとパスワードを確認してください。", }; } },};6-3. サインアップページ
src/routes/register/+page.svelte を新規作成して、以下を記述します:
<script lang="ts"> import type { ActionData } from "./$types"
export let form: ActionData
let email = "" let password = "" let passwordConfirm = "" let name = ""</script>
<div class="flex items-center justify-center min-h-screen bg-gray-100"> <div class="bg-white p-8 rounded shadow-md w-96"> <h1 class="text-2xl font-bold mb-6">サインアップ</h1>
<form method="POST" action="?/register"> <div class="mb-4"> <label for="name" class="block text-sm font-medium"> ユーザー名 </label> <input type="text" name="name" id="name" bind:value={name} class="mt-1 w-full px-3 py-2 border border-gray-300 rounded" required /> </div>
<div class="mb-4"> <label for="email" class="block text-sm font-medium"> メールアドレス </label> <input type="email" name="email" id="email" bind:value={email} class="mt-1 w-full px-3 py-2 border border-gray-300 rounded" required /> </div>
<div class="mb-4"> <label for="password" class="block text-sm font-medium"> パスワード </label> <input type="password" name="password" id="password" bind:value={password} class="mt-1 w-full px-3 py-2 border border-gray-300 rounded" required /> </div>
<div class="mb-4"> <label for="passwordConfirm" class="block text-sm font-medium"> パスワード(確認) </label> <input type="password" name="passwordConfirm" id="passwordConfirm" bind:value={passwordConfirm} class="mt-1 w-full px-3 py-2 border border-gray-300 rounded" required /> </div>
{#if form?.error} <div class="mb-4 text-red-600 text-sm">{form.error}</div> {/if}
<button type="submit" class="w-full bg-green-500 text-white py-2 rounded hover:bg-green-600" > サインアップ </button> </form>
<p class="mt-4 text-sm text-center"> 既にアカウントをお持ちですか? <a href="/login" class="text-blue-500 hover:underline"> ログイン </a> </p> </div></div>6-4. サインアップアクション
src/routes/register/+page.server.ts を新規作成して、以下を記述します:
import { prisma } from "$lib/server/db";import { hashPassword } from "$lib/server/password";import { redirect } from "@sveltejs/kit";import type { Actions } from "./$types";
export const actions: Actions = { register: async ({ request }) => { const formData = await request.formData(); const email = formData.get("email") as string; const password = formData.get("password") as string; const passwordConfirm = formData.get("passwordConfirm") as string; const name = formData.get("name") as string;
// バリデーション if (!email || !password || !passwordConfirm || !name) { return { error: "全ての項目を入力してください" }; }
if (password !== passwordConfirm) { return { error: "パスワードが一致していません" }; }
if (password.length < 8) { return { error: "パスワードは8文字以上である必要があります" }; }
// メールアドレスが既に登録されているか確認 const existingUser = await prisma.user.findUnique({ where: { email }, });
if (existingUser) { return { error: "このメールアドレスは既に登録されています" }; }
// パスワードをハッシュ化 const hashedPassword = await hashPassword(password);
// ユーザーを作成 try { await prisma.user.create({ data: { email, password: hashedPassword, name, }, });
redirect(303, "/login"); } catch (error) { return { error: "ユーザーの作成に失敗しました" }; } },};6-5. ダッシュボードページ(保護されたページ)
src/routes/dashboard/+page.svelte を新規作成して、以下を記述します:
<script lang="ts"> import { signOut } from "../../auth" import type { PageData } from "./$types"
export let data: PageData</script>
<div class="min-h-screen bg-gray-100 p-6"> <div class="max-w-4xl mx-auto bg-white rounded shadow-md p-8"> <h1 class="text-3xl font-bold mb-4"> ようこそ、{data.session?.user?.name}さん </h1>
<div class="mb-6"> <p class="text-gray-600"> メールアドレス: {data.session?.user?.email} </p> </div>
<form method="POST" action="?/logout"> <button type="submit" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600" > ログアウト </button> </form> </div></div>6-6. ダッシュボードサーバーアクション
src/routes/dashboard/+page.server.ts を新規作成して、以下を記述します:
import { redirect } from "@sveltejs/kit";import { signOut } from "../../auth";import type { PageServerLoad, Actions } from "./$types";
export const load: PageServerLoad = async (event) => { const session = await event.locals.auth();
if (!session?.user) { redirect(303, "/login"); }
return { session, };};
export const actions: Actions = { logout: async (event) => { await signOut({ redirect: false }); redirect(303, "/login"); },};6-7. ホームページ
src/routes/+page.svelte を編集して、以下を記述します:
<script lang="ts"> import type { PageData } from "./$types"
export let data: PageData</script>
<div class="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center"> <div class="text-center text-white"> <h1 class="text-5xl font-bold mb-4">ようこそ!</h1> <p class="text-xl mb-8">シンプルなアカウント管理アプリ</p>
{#if data.session?.user} <a href="/dashboard" class="bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-200" > ダッシュボードへ </a> {:else} <div class="space-x-4"> <a href="/login" class="inline-block bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-200" > ログイン </a> <a href="/register" class="inline-block bg-transparent border-2 border-white text-white px-6 py-3 rounded-lg font-semibold hover:bg-white hover:text-blue-600" > サインアップ </a> </div> {/if} </div></div>6-8. ホームページサーバーロード
src/routes/+page.server.ts を新規作成して、以下を記述します:
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async (event) => { const session = await event.locals.auth();
return { session, };};第7章:コードの矛盾チェック
チェックリスト
✅ 1. 環境変数の確認
.env ファイルに以下が存在するか確認:
DATABASE_URL… SQLite ファイルへのパスAUTH_SECRET… 32文字以上のランダム文字列AUTH_TRUST_HOST="true"
✅ 2. Prisma スキーマとデータベース
prisma/schema.prisma で定義したモデルと .env の DATABASE_URL が一致しているか確認。
✅ 3. Auth.js の設定
src/auth.ts で以下が正しく設定されているか確認:
authorize関数でパスワードハッシュを使用している- Prisma Adapter が正しく初期化されている
- Credentials プロバイダーが使用されている
✅ 4. ページルートの一貫性
- ログインフォームの
action属性が正しいルートを指している signIn、signOut関数が正しくインポートされている- ダッシュボードページがセッション確認を実施している
✅ 5. データベースアクセス
- Prisma クライアントが
$lib/server/db.tsで正しく初期化されている - サーバーアクション内でのみデータベースアクセスが行われている(クライアント側からは不可)
✅ 6. パスワード処理
- ユーザー登録時に
hashPassword()でパスワードを暗号化 - ログイン時に
verifyPassword()で暗号化されたパスワードを検証
✅ 7. Tailwind CSS
src/app.css に @import "tailwindcss"; が記述されているか確認
第8章:動作確認と実行
8-1. 開発サーバーの起動
bun run dev8-2. 新規ユーザーの登録
ブラウザで http://localhost:5173/register にアクセスして、新しいユーザーを登録します。
登録情報の例:
- ユーザー名:
太郎 - メール:
taro@example.com - パスワード:
Password123
8-3. ログインの確認
http://localhost:5173/login でログインして、メールアドレスとパスワードでログインできるか確認します。
8-4. ダッシュボードの表示
ログイン成功後、ダッシュボード(/dashboard)に遷移してユーザー情報が表示されるか確認します。
8-5. ログアウトの確認
ダッシュボードのログアウトボタンをクリックして、ログイン画面に戻るか確認します。
第9章:本番環境ビルド
9-1. 本番用にビルド
bun run build9-2. ビルド結果の確認
.svelte-kit/output ディレクトリが作成されます。
9-3. 本番環境での実行
bun run previewよくある質問(FAQ)
Q1: パスワードを忘れた場合はどうするの?
A: 本ガイドの基本機能には含まれていません。必要な場合は、以下を実装してください:
- パスワードリセット用のメール送信機能
- リセットトークンの管理(Prisma モデルに追加)
Q2: OAuth(Google、GitHub ログイン)を追加したい
A: Auth.js は OAuth をサポートしています。src/auth.ts に以下を追加:
import Google from "@auth/sveltekit/providers/google";
// providers 配列に追加Google({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET,});Q3: ユーザープロファイルページを作りたい
A: Prisma モデルに profile フィールドを追加し、新しいルート src/routes/profile/+page.svelte を作成してください。
Q4: Bun を Node.js に変更したい
A: コマンドを以下に置き換えてください:
npm install # bun install の代わりnpm run dev # bun run dev の代わりトラブルシューティング
エラー: “Cannot find module ‘$env/static/private’”
解決法: tsconfig.json に以下を追加してください:
{ "include": [".svelte-kit/ambient.d.ts"]}エラー: “DATABASE_URL is not set”
解決法: .env ファイルが DATABASE_URL を定義しているか確認してください。
ログインできない
解決法:
- メールアドレスが正確か確認
- パスワードが登録時と一致しているか確認
- ブラウザのコンソールでエラーメッセージを確認
Tailwind CSS が適用されない
解決法: 以下を確認:
src/routes/+layout.svelteでsrc/app.cssがインポートされているかvite.config.tsにtailwindcss()プラグインが登録されているか- 開発サーバーを再起動
まとめ
このガイドで、以下が実装されました:
✅ Bun でのプロジェクト管理 ✅ TypeScript による型安全なコード ✅ SvelteKit でのルーティングとページ作成 ✅ Prisma によるデータベース操作 ✅ Auth.js でのユーザー認証 ✅ Tailwind CSS でのスタイリング ✅ 最低限のアカウント管理機能
次のステップとして、エラーハンドリングの改善、ユーザープロフィール機能の追加、メール認証など、さらに高度な機能を実装できます。
公式ドキュメントをぜひ参考にしてください:
- Bun: https://bun.sh/docs
- SvelteKit: https://svelte.dev/docs/kit
- Prisma: https://www.prisma.io/docs
- Auth.js: https://authjs.dev
- Tailwind CSS: https://tailwindcss.com/docs