Next.js 15 実践:週末で本番級ブログシステムを構築した方法
はじめに
Next.js 15 がリリースされて間もなく、公式ドキュメントを何度も読み、チュートリアル動画もたくさん見ました。それでも机上の空論のまま——Server Actions や App Router の概念は分かるのに、実プロジェクトでの使い方が見えませんでした。週末、もうチュートリアルはやめて、本物のプロジェクトに取り組むことに。フルスタックのブログシステムです。フロント、バック、DB、デプロイまで。2 日後、Vercel へのデプロイに成功し、Lighthouse 性能スコアは 96 点でした。
この記事では、その過程を共有します。コードをコピペするだけのチュートリアルではなく、なぜそうしたか、どこでつまずいたかを理解できる内容にしました。技術スタックは Next.js 15 + Server Actions + Prisma + PostgreSQL。いずれも本番で使えるコードです。
なぜ Next.js 15 なのか?
技術選定は、かなり悩みました。
Next.js 15 が出た当初、コミュニティでは「またアップデートか、追いつけない」という声もありました。正直、Next.js 14 もまだ完全には使いこなせていない中、抵抗感がありました。でも新機能を詳しく見るうちに、今回のアップグレードは本物だと分かりました。
Server Actions:API Routes の煩わしさから解放
以前のフルスタック開発で一番面倒だったのが、API Routes の記述でした。api/posts/route.ts を作り、POST を定義し、リクエストボディを処理し、レスポンスを返す……毎回同じ流れで、うんざりしていました。
Server Actions はこのやり方を変えます。関数の前に 'use server' を付けるだけで、コンポーネントから直接呼び出せます。最初は「バックエンドのコードをフロントに書いているだけ?」と思いました。一度試すと——本当に楽でした。
例えば、以前は記事作成をこう書いていました:
// 旧方式:API Route が必要
// app/api/posts/route.ts
export async function POST(request: Request) {
const body = await request.json()
// 処理ロジック...
}
// フロントから fetch
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(data)
})
今はこうです:
// app/actions/post-actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
// DB を直接操作
return await prisma.post.create({
data: { title, content }
})
}
コンポーネントからは、ローカル関数を呼ぶ感覚です。たとえるなら、以前は自分でレストランまでテイクアウトを取りに行っていた(API Routes)のが、今はデリバリーで届く(Server Actions)イメージです。
Turbopack で開発速度が倍以上に
Next.js 15 で Turbopack が実験段階から安定版になりました。公式はローカルサーバー起動が 76.7% 高速、コード更新が 96.3% 高速と言います。最初はマーケ数字かと思いましたが、実際に使うと——誇張ではありませんでした。
私のプロジェクトは 30 以上のコンポーネントがあり、Webpack だと起動に 7〜8 秒かかっていました。Turbopack では 2 秒以内です。コードを変えると、ホットリロードはほぼ一瞬。開発体験の向上は、計り知れません。
なぜこのスタックがブログ向きなのか?
Next.js をブログに選んだ理由は、主に 3 つです。
SSR/SSG の天然の強み——ブログで最重要なのは SEO です。Next.js のサーバーサイドレンダリングと静的生成は、この用途のために設計されています。Google のクローラーは完成した HTML をそのまま取得でき、純クライアントレンダリングの React より圧倒的に有利です。
Prisma の型安全——以前は Mongoose で MongoDB を書いていましたが、型定義は手動メンテで、すぐバグります。Prisma はスキーマから TypeScript 型を生成するので、補完が効き、ミスが減ります。
Server Actions で開発が簡潔に——API Routes を書かなくていい分、コード量は少なくとも 30% 減ります。このブログを従来方式で書いたら、route.ts がいくつも増えていたはずです。
技術スタック選定とアーキテクチャ設計
「なぜ」が分かったら、次は「どうやって」です。
私の完全な技術スタック
- フロントエンド: Next.js 15 + TypeScript + Tailwind CSS
- バックエンド: Next.js Server Actions + NextAuth.js
- データベース: PostgreSQL + Prisma ORM
- デプロイ: Vercel
MongoDB では?と思うかもしれません。最初は私もそう考えていました。MongoDB は柔軟です。でも Prisma の PostgreSQL サポートの方が充実しており、記事・ユーザー・コメントのように関係が明確なデータには、リレーショナル DB の方が向いていると判断しました。
ディレクトリ構造
my-blog/
├── app/
│ ├── (auth)/ # 認証関連ページ
│ ├── blog/ # ブログ関連ページ
│ │ └── [slug]/ # 動的ルート
│ ├── dashboard/ # ユーザーダッシュボード
│ ├── actions/ # Server Actions を集約
│ └── api/auth/ # NextAuth 設定
├── components/ # 再利用コンポーネント
├── lib/
│ ├── prisma.ts # Prisma シングルトン(重要!)
│ └── utils.ts # ユーティリティ
├── prisma/
│ └── schema.prisma # DB スキーマ
└── public/ # 静的アセット
この構造は何度か改訂しました。最初は Server Actions を各ページに散らばらせていましたが、メンテが大変で、actions ディレクトリにまとめたらすっきりしました。
データベーススキーマ設計
DB 設計では大きなつまずきがありました。初版はタグ、カテゴリ、閲覧数など盛りすぎて、半ばで「要らないテーブルが多い」と気づき、半日かけて簡素化しました。
最終的なコア Schema はこうです:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
image String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String @db.Text
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([slug])
@@index([authorId])
}
2 つの @@index に注目してください。性能最適化の要です。ブログ一覧では slug で検索し、ユーザーページでは authorId で絞り込みます。インデックスを付けるだけで、クエリ速度は数倍になります。
コア機能の実装詳細
アーキテクチャが決まったら、いよいよコードです。
3.1 環境構築と初期化
Next.js 15 プロジェクトの作成は簡単です:
npx create-next-app@latest my-blog
cd my-blog
npm install prisma @prisma/client zod next-auth
Prisma の初期化:
npx prisma init
prisma/schema.prisma と .env が生成されます。.env に PostgreSQL 接続を設定します:
DATABASE_URL="postgresql://username:password@localhost:5432/myblog?schema=public"
ローカル開発は Docker で PostgreSQL を立てるのが手軽です:
docker run --name blog-postgres -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres
3.2 Prisma データベース連携
Schema を書いたらマイグレーションを実行:
npx prisma migrate dev --name init
ここで注意——Prisma Client のシングルトンパターンです。初めて Vercel にデプロイしたとき、この設定を忘れていて、半日で “Too many connections” エラーが出ました。DDoS かと思うほどでした。
原因は、Next.js 開発環境のホットリロードが、更新のたびに新しい Prisma Client インスタンスを作り、接続プールを枯渇させていたことです。正しい実装は次のとおり:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
少し回りくどく見えますが、開発環境では Prisma インスタンスが 1 つだけになることを保証します。本番への影響はありません。Prisma 公式の推奨パターンです。
3.3 Server Actions の実践
記事作成がコア機能です。Server Actions の書き方を見てみましょう:
// app/actions/post-actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// Zod でバリデーション
const PostSchema = z.object({
title: z.string().min(1, 'タイトルは必須です').max(100),
content: z.string().min(10, '本文は 10 文字以上必要です'),
slug: z.string().regex(/^[a-z0-9-]+$/, 'slug は小文字・数字・ハイフンのみ')
})
export async function createPost(formData: FormData) {
const validatedFields = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
slug: formData.get('slug')
})
if (!validatedFields.success) {
return { error: 'バリデーションに失敗しました' }
}
try {
const post = await prisma.post.create({
data: {
...validatedFields.data,
authorId: 'user-id-here' // 実運用では session から取得
}
})
revalidatePath('/blog')
return { success: true, post }
} catch (error) {
return { error: '記事の作成に失敗しました' }
}
}
コンポーネント側:
// app/dashboard/new-post/page.tsx
import { createPost } from '@/app/actions/post-actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" placeholder="タイトル" />
<textarea name="content" placeholder="本文" />
<input name="slug" placeholder="URL slug" />
<button type="submit">公開</button>
</form>
)
}
useState も fetch も onSubmit も不要です。フォームが Server Action を直接呼び、Next.js がシリアライズ、ネットワーク、エラー処理を担当します。
Server Actions と API Routes の違いが最初は分かりにくかったのですが、Server Actions は「デリバリーで届く」、API Routes は「自分で店に行く」イメージです。多くの場面では Server Actions で足ります。
3.4 ユーザー認証システム
認証には NextAuth.js を使いました。設定はシンプルです:
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHubProvider from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const authOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!
})
]
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
GitHub OAuth は Developer Settings でアプリを作り、Client ID と Secret を .env に入れるだけ。10 分程度。JWT 認証を自前で書いていた頃が遠い昔に感じます。
3.5 SSR/SSG 最適化戦略
性能最適化の要です。最初は全部 SSR にしていましたが、ブログ一覧を開くたび DB を叩くのは無駄だと気づき、ハイブリッド戦略に切り替えました。
ブログ一覧——SSG + ISR:
// app/blog/page.tsx
import { prisma } from '@/lib/prisma'
export const revalidate = 3600
export default async function BlogList() {
const posts = await prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' }
})
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content.slice(0, 150)}...</p>
</article>
))}
</div>
)
}
ビルド時に静的 HTML を生成し、1 時間ごとに再生成します。ユーザーは静的ファイルをそのまま受け取るので、非常に速いです。
ブログ詳細——動的 SSR:
// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
const post = await prisma.post.findUnique({
where: { slug: params.slug }
})
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
アクセスのたび DB から最新内容を取得。更新が多い記事向きです。
性能比較データ:
差は歴然です。Next.js がブログ向きな理由——SEO と性能を両立できるからです。
デプロイと最適化
コードができたら、いよいよデプロイです。
Vercel デプロイの流れ
GitHub にプッシュしたあと、Vercel でリポジトリをインポート。Next.js プロジェクトと判定されれば、ほぼ自動設定されます。手動で必要なのは環境変数です:
DATABASE_URL="本番 DB 接続文字列"
GITHUB_ID="OAuth Client ID"
GITHUB_SECRET="OAuth Secret"
NEXTAUTH_URL="https://your-domain.vercel.app"
NEXTAUTH_SECRET="ランダム文字列"
本番 DB には Supabase の無料 PostgreSQL を使いました。月 500MB 枠があり、個人ブログなら十分です。
Deploy をクリックして約 2 分。自分のドメインで初めてアクセスできたとき、画面を数秒見つめて、思わずスクショを SNS に載せました。
本番環境の最適化
デプロイ成功はゴールではありません。あと少し最適化があります。
Prisma 接続プール設定:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
Serverless では関数呼び出しごとに新接続が作られるため、directUrl でプールを再利用します。
画像最適化:
import Image from 'next/image'
<Image
src="/avatar.jpg"
alt="ユーザーアバター"
width={100}
height={100}
/>
Next.js の Image コンポーネントは WebP 変換とオンデマンド読み込みを自動で行い、性能向上がはっきり分かります。
最終性能スコア:
Lighthouse で計測したところ、デスクトップ 96 点、モバイル 92 点、SEO 100 点。2 日でゼロから本番級まで持っていけた達成感は、言葉にしにくいものでした。
拡張の方向性と学習リソース
現状のブログは MVP です。まだ足せる機能はたくさんあります。
- Markdown エディタ: react-md-editor の統合
- コメント: Giscus(GitHub Discussions ベース)
- 検索: Algolia、または Prisma の全文検索
- ダークモード: next-themes で比較的簡単
次は Markdown エディタを入れて、執筆を楽にしたいと思っています。
おすすめ学習リソース:
- Next.js 公式ドキュメント — いつでも最も信頼できる資料
- Prisma ドキュメント — Getting Started を一通り読めば十分
- このプロジェクトの完全コード — clone してそのまま動かせます
十回読むより、一度手を動かす方が効きます。理論は分かっても、実戦で初めて身につきます。
まとめ
この週末プロジェクトで学んだこと:
- Server Actions の強さ: 多くの API Routes を代替でき、開発効率は段違い
- Prisma の型安全: TypeScript サポートが揃い、コードを書く安心感がある
- SSG/SSR の柔軟性: シーンに合わせて描画戦略を選べば、性能と SEO を両立できる
- Vercel デプロイの手軽さ: コードから公開まで、5 分もかからない
正直、最大の収穫は技術そのものより自信でした。フルスタック開発も、手を動かせば意外と難しくない——そう感じられました。
自分のブログを作りたいなら、迷わず始めてください。つまずくのは普通です。私もドキュメントを見ながら無数の穴に落ちました。でも公開できた瞬間、すべて報われます。
この記事が、少しでも遠回りを減らす助けになれば嬉しいです。コメントで質問があれば、できる限り返します。あなたの作品も楽しみにしています。
Next.js 15 で本番級ブログシステムを構築する完全フロー
環境構築からデプロイまで。Server Actions、Prisma、SSG/SSR 最適化などコア機能を含む手順
Estimated time: PT16H
-
1
Step 1: 環境構築とプロジェクト初期化
Next.js 15 プロジェクト作成: -
2
Step 2: Prisma DB スキーマ設定
コア Schema 設計: -
3
Step 3: Server Actions コア機能
Server Actions ファイル作成: -
4
Step 4: ユーザー認証設定
NextAuth.js: -
5
Step 5: SSR/SSG 描画戦略の最適化
ブログ一覧は SSG + ISR:app/blog/page.tsx で export const revalidate = 3600(1 時間ごと再生成)。prisma.post.findMany で公開記事を createdAt 降順取得。ビルド時に静的 HTML を生成し、1 時間ごとに再生成。ユーザーは静的ファイルを受け取り、読み込み約 200ms。記事詳細は動的 SSR:app/blog/[slug]/page.tsx でアクセスごとに DB から最新を取得。prisma.post.findUnique({ where: { slug: params.slug } })。更新が多い記事向き、読み込み約 800ms。性能比較:SSG 〜200ms、SSR 〜800ms、純 CSR 〜1500ms。差は歴然。 -
6
Step 6: Vercel デプロイと最適化
GitHub にプッシュ:プロジェクトを GitHub リポジトリへ。Vercel でインポート:GitHub リポジトリを Vercel に連携。Next.js と判定されれば自動設定。環境変数:DATABASE_URL(本番 DB)、GITHUB_ID、GITHUB_SECRET、NEXTAUTH_URL(https://your-domain.vercel.app)、NEXTAUTH_SECRET(ランダム文字列)。本番 DB は Supabase 無料 PostgreSQL 推奨(月 500MB、個人ブログ向き)。Prisma 接続プール:prisma/schema.prisma の datasource に directUrl = env("DIRECT_URL")。Serverless では関数ごとに新接続が作られるため、directUrl でプール再利用。画像最適化:Next.js Image コンポーネントで WebP 変換、オンデマンド読み込み。Deploy 後約 2 分で公開。Lighthouse:デスクトップ 96 点、モバイル 92 点、SEO 100 点。
FAQ
Next.js 15 の Server Actions と API Routes の違いは?どちらを使うべき?
Server Actions の利点:
1) コード量が 30% 減り、API Routes を書く必要がない
2) コンポーネントから直接呼び出せ、`useState`、`fetch`、`onSubmit` の処理が不要
3) Next.js がシリアライズ、ネットワーク、エラー処理を自動で担当
4) 型安全で、TypeScript の型をそのまま使える
使用例:関数の前に `'use server'` を付け、コンポーネントから `<form action={createPost}>` のように呼び出す。
API Routes の利点:
• より柔軟で、複雑なリクエスト処理に対応
• ミドルウェアをサポート
• カスタム HTTP レスポンスが必要な場面向き
多くのケースでは Server Actions で十分。複雑な HTTP 処理が必要なときだけ API Routes を検討しましょう。
Turbopack の性能向上はどの程度?実際の体感は?
公式データ:
• ローカルサーバー起動が 76.7% 高速
• コード更新が 96.3% 高速
実際の体感:
• 私のプロジェクトは 30 以上のコンポーネントがあり、Webpack だと起動に 7〜8 秒。Turbopack では 2 秒以内
• コードを変えると、ホットリロードはほぼ一瞬
• 開発体験の向上は、言葉にしにくいほど大きい
Turbopack は Rust 製で Webpack より速く、大規模プロジェクトほど差が出ます。まだ Webpack のままなら、Next.js 15 へ上げて Turbopack を試す価値があります。
Prisma Client のシングルトンパターンはなぜ重要?設定しないとどうなる?
問題のシナリオ:
• Next.js の開発環境はホットリロードのたびに新しい Prisma Client インスタンスを作る
• 接続プールがすぐに枯渇し、「Too many connections」エラーになる
• 初めて Vercel にデプロイしたとき、この設定を忘れていて、半日でサイトが落ちた。DDoS 攻撃かと思った
正しい設定:
• `lib/prisma.ts` でグローバルシングルトンを実装:
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
このコードで、開発環境では Prisma インスタンスが 1 つだけになり、本番環境への影響はありません。覚えておいてください。これは Prisma 公式の推奨パターンです。
SSG、SSR、ISR の違いは?どう選ぶ?
• ビルド時に静的 HTML を生成。最速(〜200ms)
• 更新頻度が低いページ向き(ブログ一覧、About など)
SSR(Server-Side Rendering):
• リクエストのたびにサーバーで HTML を生成
• リアルタイムデータが必要なページ向き(記事詳細、ユーザーページ)
• 読み込みは約 800ms
ISR(Incremental Static Regeneration):
• SSG の速度と SSR の柔軟性を組み合わせ、一定間隔で再生成
• たまに更新するページ向き(例:ブログ一覧を 1 時間ごとに更新)
純 CSR(Client-Side Rendering):
• ブラウザで描画。SEO が弱く、読み込みは〜1500ms
• ブログには非推奨
選び方:
• ブログ一覧は SSG + ISR(`export const revalidate = 3600`)
• 記事詳細は動的 SSR
• About は純 SSG
性能比較:SSG 〜200ms、SSR 〜800ms、純 CSR 〜1500ms。差は歴然です。
なぜ PostgreSQL を選び、MongoDB ではない?Prisma の PostgreSQL サポートは?
1) Prisma の PostgreSQL サポートが充実。型安全、マイグレーション、クエリ最適化が揃っている
2) 記事・ユーザー・コメントのように関係がはっきりしたデータには、リレーショナル DB の方が向いている
3) PostgreSQL の全文検索が強く、ブログ検索に適する
4) 本番環境の安定性とトランザクションサポートが優れている
Prisma の利点:
• スキーマから TypeScript 型を自動生成。補完が効き、ミスが減る
• 以前 Mongoose で MongoDB を書いていたが、型定義は手動メンテで、すぐバグる
• Prisma の型安全さは、コードを書く安心感につながる
複雑なリレーションデータなら、PostgreSQL + Prisma は有力な選択です。
Vercel への Next.js デプロイで注意点は?本番環境の最適化は?
1) コードを GitHub にプッシュ
2) Vercel で GitHub リポジトリをインポート。Next.js プロジェクトと判定されれば自動設定
3) 環境変数を設定(DATABASE_URL、GITHUB_ID、GITHUB_SECRET、NEXTAUTH_URL、NEXTAUTH_SECRET)
4) Deploy をクリック。約 2 分で公開
本番環境の最適化:
1) Prisma 接続プール:
• `prisma/schema.prisma` の datasource に `directUrl = env("DIRECT_URL")` を追加
• Serverless では関数呼び出しごとに新接続が作られるため、`directUrl` でプールを再利用
2) 画像最適化:
• Next.js の Image コンポーネントで WebP 変換、オンデマンド読み込み。効果は明確
3) 本番 DB は Supabase の無料 PostgreSQL がおすすめ(月 500MB、個人ブログなら十分)
最終スコア:
• Lighthouse で計測したところ、デスクトップ性能 96 点、モバイル 92 点、SEO 100 点
• 2 日でゼロから本番級の性能まで持っていけたとき、達成感はひとしお
6分で読めます · 公開日: 2025年11月24日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 初心者向け完全ガイド。Server Components、Client Components、特殊ファイルなどのコア概念を実例付きで解説し、新ルーティングシステムを素早く習得して回り道を減らします。
第 1 / 47 記事
次の記事
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
本番環境のバグから完全な解決策まで、Next.js Middleware の matcher 設定、Edge Runtime の制限、認証・国際化・A/B テストの 3 大シナリオを解説し、最も陥りやすい罠の回避法をまとめます
第 3 / 47 記事
関連記事
Next.js を Vercel にデプロイする完全ガイド:環境変数、ドメイン設定、パフォーマンス監視
Next.js を Vercel にデプロイする完全ガイド:環境変数、ドメイン設定、パフォーマンス監視
Next.js データベース選定ガイド:PostgreSQL、MySQL、MongoDB とクラウドサービスの完全比較
Next.js データベース選定ガイド:PostgreSQL、MySQL、MongoDB とクラウドサービスの完全比較
Next.js 高度なルーティング実践:Route Groups・ネストレイアウト・Parallel Routes・Intercepting Routes 完全ガイド
コメント
GitHubアカウントでログインしてコメントできます