SolidStart フレームワーク入門ガイド


SolidStartは、SolidJSをベースとしたフルスタックWebアプリケーションフレームワークです。Next.jsやRemixのような開発体験を提供しながら、SolidJSの優れたパフォーマンスとリアクティビティを活かせます。本記事では、SolidStartの基礎から実践的な使い方まで詳しく解説します。

SolidStartとは

SolidStartは、SolidJSの公式メタフレームワークで、以下の機能を提供します。

主な特徴

1. ファイルベースルーティング

  • 直感的なルート定義
  • ネストされたルート対応

2. SSRとSSG対応

  • サーバーサイドレンダリング
  • 静的サイト生成
  • ハイブリッドレンダリング

3. API Routes

  • サーバーサイドAPIエンドポイント
  • タイプセーフなデータフェッチ

4. 高性能

  • 細粒度リアクティビティ
  • 最小限のJavaScriptバンドル

セットアップ

プロジェクト作成

# 新規プロジェクト作成
npm create solid@latest my-solid-app

# オプション選択
# ✔ Which template would you like to use? › bare
# ✔ Use TypeScript? … Yes
# ✔ Server Side Rendering? … Yes

cd my-solid-app
npm install

プロジェクト構造

my-solid-app/
├── src/
│   ├── routes/
│   │   └── index.tsx
│   ├── components/
│   ├── app.tsx
│   └── entry-client.tsx
├── public/
├── app.config.ts
└── package.json

開発サーバー起動

npm run dev
# http://localhost:3000

ルーティング

基本的なルート

ファイル構造がそのままURLになります。

src/routes/
├── index.tsx           → /
├── about.tsx           → /about
├── blog/
│   ├── index.tsx       → /blog
│   └── [id].tsx        → /blog/:id
└── users/
    └── [id]/
        └── posts.tsx   → /users/:id/posts

ページコンポーネント

src/routes/index.tsx:

export default function Home() {
  return (
    <main>
      <h1>Welcome to SolidStart</h1>
      <p>Build fast, reactive web apps</p>
    </main>
  );
}

動的ルート

src/routes/blog/[id].tsx:

import { useParams } from "@solidjs/router";

export default function BlogPost() {
  const params = useParams();

  return (
    <article>
      <h1>Blog Post: {params.id}</h1>
    </article>
  );
}

ネストされたレイアウト

src/routes/blog.tsx:

import { Outlet } from "@solidjs/router";

export default function BlogLayout() {
  return (
    <div class="blog-layout">
      <aside>
        <h2>Blog Navigation</h2>
        <nav>
          <a href="/blog">All Posts</a>
          <a href="/blog/categories">Categories</a>
        </nav>
      </aside>
      <main>
        <Outlet /> {/* 子ルートがここに表示される */}
      </main>
    </div>
  );
}

データフェッチング

createRouteData

import { createRouteData } from "@solidjs/router";
import { For } from "solid-js";

// データフェッチング関数
const fetchPosts = async () => {
  const response = await fetch("https://api.example.com/posts");
  return response.json();
};

export function routeData() {
  return createRouteData(fetchPosts);
}

export default function Blog() {
  const posts = useRouteData<typeof routeData>();

  return (
    <div>
      <h1>Blog Posts</h1>
      <For each={posts()}>
        {(post) => (
          <article>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </article>
        )}
      </For>
    </div>
  );
}

パラメータ付きデータフェッチ

import { useParams } from "@solidjs/router";
import { createRouteData } from "@solidjs/router";

const fetchPost = async (id: string) => {
  const response = await fetch(`https://api.example.com/posts/${id}`);
  return response.json();
};

export function routeData() {
  const params = useParams();
  return createRouteData(
    () => params.id,
    { key: () => params.id }
  );
}

export default function BlogPost() {
  const post = useRouteData<typeof routeData>();

  return (
    <article>
      <h1>{post()?.title}</h1>
      <div innerHTML={post()?.content} />
    </article>
  );
}

Server Functions

サーバーサイドでのみ実行される関数を定義できます。

基本的な使用

import { createServerData$ } from "solid-start/server";

// サーバー関数
const getUsers = async () => {
  "use server"; // サーバー専用マーカー

  // データベースアクセスなど
  const users = await db.query("SELECT * FROM users");
  return users;
};

export function routeData() {
  return createServerData$(getUsers);
}

export default function Users() {
  const users = useRouteData<typeof routeData>();

  return (
    <div>
      <h1>Users</h1>
      <For each={users()}>
        {(user) => (
          <div>
            <h2>{user.name}</h2>
            <p>{user.email}</p>
          </div>
        )}
      </For>
    </div>
  );
}

Server Actions

import { createServerAction$ } from "solid-start/server";

const createUser = async (formData: FormData) => {
  "use server";

  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  // データベースに保存
  await db.users.insert({ name, email });

  return { success: true };
};

export default function CreateUser() {
  const [submitting, submit] = createServerAction$(createUser);

  return (
    <form action={submit} method="post">
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={submitting.pending}>
        {submitting.pending ? "Creating..." : "Create User"}
      </button>
    </form>
  );
}

API Routes

src/routes/api/users.ts:

import { json } from "solid-start";
import type { APIEvent } from "solid-start";

export async function GET({ request }: APIEvent) {
  const users = await db.users.findMany();
  return json(users);
}

export async function POST({ request }: APIEvent) {
  const data = await request.json();
  const user = await db.users.create(data);
  return json(user, { status: 201 });
}

複雑なAPI:

import { json } from "solid-start";
import type { APIEvent } from "solid-start";

// GET /api/users/:id
export async function GET({ params }: APIEvent) {
  const user = await db.users.findById(params.id);

  if (!user) {
    return json(
      { error: "User not found" },
      { status: 404 }
    );
  }

  return json(user);
}

// PUT /api/users/:id
export async function PUT({ params, request }: APIEvent) {
  const data = await request.json();
  const user = await db.users.update(params.id, data);
  return json(user);
}

// DELETE /api/users/:id
export async function DELETE({ params }: APIEvent) {
  await db.users.delete(params.id);
  return new Response(null, { status: 204 });
}

フォーム処理

基本的なフォーム

import { createServerAction$ } from "solid-start/server";
import { Show } from "solid-js";

const handleLogin = async (formData: FormData) => {
  "use server";

  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  // 認証処理
  const user = await authenticate(email, password);

  if (!user) {
    throw new Error("Invalid credentials");
  }

  return { user };
};

export default function Login() {
  const [loggingIn, login] = createServerAction$(handleLogin);

  return (
    <form action={login} method="post">
      <h1>Login</h1>

      <Show when={loggingIn.error}>
        <p class="error">{loggingIn.error.message}</p>
      </Show>

      <input
        name="email"
        type="email"
        placeholder="Email"
        required
      />

      <input
        name="password"
        type="password"
        placeholder="Password"
        required
      />

      <button type="submit" disabled={loggingIn.pending}>
        {loggingIn.pending ? "Logging in..." : "Login"}
      </button>
    </form>
  );
}

プログレッシブエンハンスメント

import { createServerAction$ } from "solid-start/server";
import { Show } from "solid-js";

export default function TodoForm() {
  const [adding, add] = createServerAction$(async (formData: FormData) => {
    "use server";

    const title = formData.get("title") as string;
    await db.todos.create({ title });
  });

  return (
    <form action={add} method="post">
      <input
        name="title"
        placeholder="New todo..."
        required
      />

      <button type="submit">
        Add Todo
      </button>

      {/* JavaScriptが有効な場合のみ表示 */}
      <Show when={adding.pending}>
        <span>Adding...</span>
      </Show>
    </form>
  );
}

メタデータとSEO

Titleコンポーネント

import { Title, Meta } from "@solidjs/meta";

export default function About() {
  return (
    <>
      <Title>About Us - My Site</Title>
      <Meta name="description" content="Learn more about our company" />
      <Meta property="og:title" content="About Us" />
      <Meta property="og:description" content="Learn more about our company" />

      <main>
        <h1>About Us</h1>
        <p>Welcome to our about page</p>
      </main>
    </>
  );
}

動的メタデータ

import { Title, Meta } from "@solidjs/meta";
import { useRouteData } from "solid-start";

export default function BlogPost() {
  const post = useRouteData<typeof routeData>();

  return (
    <>
      <Title>{post()?.title} - My Blog</Title>
      <Meta name="description" content={post()?.excerpt} />
      <Meta property="og:title" content={post()?.title} />
      <Meta property="og:image" content={post()?.image} />

      <article>
        <h1>{post()?.title}</h1>
        <div innerHTML={post()?.content} />
      </article>
    </>
  );
}

ミドルウェア

src/middleware.ts:

import { createMiddleware } from "solid-start/middleware";

export default createMiddleware({
  onRequest: [
    // 認証チェック
    async (event) => {
      const token = event.request.headers.get("authorization");

      if (!token && event.request.url.includes("/protected")) {
        return new Response("Unauthorized", { status: 401 });
      }
    },

    // ログ
    async (event) => {
      console.log(`${event.request.method} ${event.request.url}`);
    },
  ],
});

デプロイ

Vercelへのデプロイ

npm install -g vercel
vercel

Cloudflare Pagesへのデプロイ

# ビルド
npm run build

# wranglerでデプロイ
npx wrangler pages deploy ./dist

Netlifyへのデプロイ

# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

パフォーマンス最適化

コード分割

import { lazy } from "solid-js";

// 遅延読み込み
const HeavyComponent = lazy(() => import("./HeavyComponent"));

export default function Page() {
  return (
    <div>
      <h1>Page</h1>
      <HeavyComponent />
    </div>
  );
}

画像最適化

import { Image } from "solid-start";

export default function Gallery() {
  return (
    <Image
      src="/large-image.jpg"
      alt="Description"
      width={800}
      height={600}
      loading="lazy"
    />
  );
}

まとめ

SolidStartは、SolidJSの優れたパフォーマンスを活かしながら、モダンなWebアプリケーション開発に必要な機能を提供するフレームワークです。

主な利点

  • 高性能: 細粒度リアクティビティによる高速レンダリング
  • タイプセーフ: TypeScriptによる型安全性
  • 柔軟性: SSR、SSG、SPAのハイブリッド対応
  • 開発体験: 直感的なAPI設計

パフォーマンスを重視しつつ、Next.jsのような開発体験を求める場合、SolidStartは優れた選択肢となるでしょう。