言語を切り替える
テーマを切り替える

Next.js 15実践:週末だけで生産性レベルのブログシステムを構築した話

はじめに

2ヶ月前の金曜日の夜のことを覚えています。会社のプロジェクトが無事リリースされ、少しリラックスして新しい技術でも学ぼうとPCを開きました。
Next.js 15がリリースされたばかりで、公式ドキュメントを読み、チュートリアル動画も見漁っていました。しかし、Server ActionsやApp Routerといった概念は頭では理解できても、「で、実際のプロジェクトでどう使うの?」というモヤモヤが晴れずにいました。
チュートリアルを見終わってブラウザを閉じた瞬間、頭が真っ白になるあの感覚、あなたにも経験がありませんか?

そこでその週末、私はチュートリアルを見るのをやめ、腕まくりをして本物のプロジェクトを作ることにしました。そう、フルスタックブログシステムです。
正直、最初は自信がありませんでした。フロントエンド、バックエンド、データベース、デプロイ……これらを全部一人で、しかも週末だけでやるなんて無謀に思えました。
しかし2日後、構築したブログがVercelに無事デプロイされ、Lighthouseのスコアで96点を叩き出した時、言葉にできない達成感を味わいました。

この記事では、その全プロセスを共有します。単なるコードのコピペ用チュートリアルではありません。「なぜその技術を選んだのか」「どこでハマったのか」という生々しい実体験をお伝えします。
使用する技術スタックは Next.js 15 + Server Actions + Prisma + PostgreSQL。これらはすべて、実際のプロダクション(本番)環境で通用する構成です。

もしあなたも週末だけでNext.jsフルスタック開発をマスターしたいなら、ぜひ私の旅に付き合ってください。

なぜ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()
  // 処理ロジック...
}
// フロントエンドでの呼び出し
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')
  // データベース操作を直接記述!
  return await prisma.post.create({
    data: { title, content }
  })
}

コンポーネント内での呼び出しは、まるでローカル関数を呼ぶのと同じ感覚です。例えるなら、**以前は自分でレストランまでテイクアウトを取りに行っていた(API Routes)のが、今はウーバーイーツが勝手に届けてくれる(Server Actions)**ようなものです。

Turbopackで開発速度が倍増

Next.js 15でTurbopackが安定版になりました。公式は「ローカルサーバー起動76.7%高速化」「コード更新96.3%高速化」と謳っていますが、これはマーケティング用語ではありません。事実でした。

76.7% UP
サーバー起動速度
公式ベンチマーク
96.3% UP
コード更新速度
ホットリロードがほぼ一瞬
2秒
プロジェクト起動
30コンポーネント規模で7-8秒→2秒未満

私のプロジェクトは30個ほどのコンポーネントがありましたが、Webpackでは起動に7-8秒かかっていたのが、Turbopackでは2秒以内に短縮されました。コード修正後の反映も一瞬です。この体験の向上は計り知れません。

なぜブログにこのスタックなのか?

  1. SSR/SSGの天然の強み:ブログにとってSEOは命です。サーバーサイドレンダリングと静的生成が得意なNext.jsは、Googleのクローラーに対して最強の相性を持っています。
  2. Prismaの型安全性:以前Mongoose(MongoDB)を使っていた時は型定義の手動メンテが地獄でしたが、PrismaはスキーマからTypeScriptの型を自動生成してくれます。開発体験が段違いです。
  3. Server Actionsによる効率化:API Routesを書かなくていいので、コード量が30%は減りました。

技術スタック選定とアーキテクチャ設計

「なぜ」がわかったところで、「どうやって」の話に入りましょう。

私の完全な技術スタック

  • フロントエンド: Next.js 15 + TypeScript + Tailwind CSS
  • バックエンド: Next.js Server Actions + NextAuth.js
  • データベース: PostgreSQL + Prisma ORM
  • デプロイ: Vercel

「なんでMongoDBじゃないの?」と思うかもしれません。MongoDBの方が柔軟なのは確かですが、PrismaはPostgreSQLとの相性が抜群に良く、記事・ユーザー・コメントといった「リレーション(関係性)が明確なデータ」には、RDB(リレーショナルデータベース)の方が適していると判断しました。

ディレクトリ構造

my-blog/
├── app/
│   ├── (auth)/          # 認証関連ページ
│   ├── blog/            # ブログ関連ページ
│   │   └── [slug]/      # 動的ルーティング
│   ├── dashboard/       # 管理画面
│   ├── actions/         # Server Actionsはここに集約
│   └── api/auth/        # NextAuth設定
├── components/          # 共通コンポーネント
├── lib/
│   ├── prisma.ts        # Prismaシングルトン(これ超重要!)
│   └── utils.ts         # ユーティリティ
├── prisma/
│   └── schema.prisma    # データベーススキーマ
└── public/              # 静的ファイル

最初はServer Actionsを各ページのファイルに書いていましたが、管理しきれなくなり actions ディレクトリにまとめました。今のところこれがベストプラクティスだと感じています。

データベーススキーマ設計

ここで一度やらかしました。最初はタグ機能、カテゴリ、閲覧数カウントなど盛り込みすぎてテーブルが複雑怪奇になり、途中で破綻しました。
最終的に落ち着いた「必要十分な」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])
}

注目してほしいのは @@index です。ブログ記事はSlugで検索され、管理画面ではAuthorIdでフィルタリングされます。このインデックスがあるだけで、クエリ速度は何倍にもなります。

コア機能の実装詳細

さあ、コードを書いていきましょう。

3.1 環境構築と初期化

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の接続文字列を設定します。
ローカル開発にはDockerを使うのが手軽です:

docker run --name blog-postgres -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres

3.2 Prismaデータベース連携

スキーマを書いたらマイグレーションを実行:

npx prisma migrate dev --name init

ここで最大のトラップがあります。「Prisma Clientのシングルトン化」です。
Vercelにデプロイした時、サイトが数時間で “Too many connections” エラーを出して落ちました。原因は、Next.jsのホットリロード機能が、コード更新のたびに新しいPrisma Clientインスタンス(=新しいDB接続)を作っていたからでした。
正しい実装はこれです:

// 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
}

このコードは「開発環境ではグローバル変数にインスタンスを保存し、再利用する」ことを保証します。これを忘れると本番環境で確実に死にます。

3.3 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' // 本番ではセッションから取得
      }
    })
    // キャッシュを更新して、新しい記事を即座に反映
    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スラッグ" />
      <button type="submit">公開</button>
    </form>
  )
}

見てください、このシンプルさ。useStatefetchonSubmit もありません。フォームが直接サーバー関数を叩く。この体験は革命的です。

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アプリを作成し、IDとSecretを環境変数に入れるだけ。10分で終わります。JWT認証を自前で実装していた頃が嘘のようです。

3.5 SSR/SSG最適化戦略

最初はすべてSSR(サーバーサイドレンダリング)にしていましたが、トップページの表示が遅いことに気づきました。毎回DBアクセスが発生していたからです。
そこで混合戦略に変更しました。

ブログ一覧ページ —— SSG + ISR:

// app/blog/page.tsx
import { prisma } from '@/lib/prisma'
// 1時間に1回再生成
export const revalidate = 3600
export default async function BlogList() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' }
  })
  return (
    <div>{/* 記事リスト表示 */}</div>
  )
}

これで、ユーザーは事前にビルドされたHTMLを受け取るだけになり、爆速になります。

ブログ詳細ページ —— 動的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>{/* 記事詳細表示 */}</article>
  )
}

こちらは常に最新の内容を表示したいので、アクセス毎にレンダリングします。

デプロイと最適化

いよいよVercelへデプロイです。GitHubリポジトリを連携するだけで自動設定されますが、環境変数の設定(DATABASE_URLなど)を忘れずに。DBはSupabaseの無料枠を使いました。

重要な最適化設定:Prismaの接続プール
Serverless環境ではDB接続数が枯渇しやすいので、Prismaの設定に directUrl を追加し、コネクションプーリングを有効にします。

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

最終的なパフォーマンス:
Lighthouseで計測した結果、デスクトップ96点、モバイル92点を記録しました。

96点
Lighthouseパフォーマンススコア

まとめ

この週末プロジェクトで学んだこと:

  1. Server Actionsは本物だ:API Routesを置き換えるに十分なパワーと利便性がある。
  2. Prismaの型安全性は依存性がある:これに慣れると、もう他のORMには戻れない。
  3. SSGとSSRの使い分けが肝:全部SSRにするのではなく、適材適所でISRを使うのがNext.jsの真骨頂。

もしあなたが「フルスタック開発」という言葉に尻込みしているなら、今すぐNext.js 15で小さなプロジェクトを始めてみてください。手を動かせば、必ず理解できます。そして、自分の書いたコードが世界に公開される瞬間の喜びを、ぜひ味わってください。

Next.js 15による生産性レベルのブログ構築フロー

環境構築からServer Actionsの実装、デプロイ、パフォーマンス最適化までの完全手順

⏱️ Estimated time: 16 hr

  1. 1

    Step1: 環境構築と初期化

    1. Next.js 15プロジェクト作成: npx create-next-app@latest my-blog
    2. 依存関係インストール: npm install prisma @prisma/client zod next-auth
    3. Prisma初期化: npx prisma init
    4. DB接続設定: .envファイルにDATABASE_URLを設定(ローカルはDocker推奨)
  2. 2

    Step2: データベースSchema設計

    UserとPostモデルを定義。
    Postモデルには、slugとauthorIdにインデックス(@@index)を追加し、クエリパフォーマンスを最適化する。
    定義後、npx prisma migrate dev --name init でDBに反映。
  3. 3

    Step3: Prisma Clientシングルトン設定

    開発環境でのホットリロードによるDB接続数枯渇を防ぐため、lib/prisma.tsでグローバル変数を用いたシングルトンパターンを実装する。これは必須設定。
  4. 4

    Step4: Server Actions実装

    app/actions/post-actions.tsを作成し、'use server'ディレクティブを付与。
    Zodで入力値を検証し、prisma.post.createでDBに保存。revalidatePathでキャッシュを更新する。
  5. 5

    Step5: レンダリング戦略の適用

    一覧ページはISR(export const revalidate = 3600)で高速化。
    詳細ページは動的SSRで常に最新情報を表示。
    これによりSEOとパフォーマンスを両立させる。
  6. 6

    Step6: デプロイと最適化

    VercelにGitHub経由でデプロイ。
    環境変数(DATABASE_URL等)を設定。
    Supabase等の外部DBを使用する場合は、PrismaでdirectUrlを設定し、コネクションプーリングを活用する。

FAQ

Server ActionsとAPI Routes、どちらを使うべき?
基本的にはServer Actionsを推奨します。コード量が減り、型安全性も高く、開発効率が良いからです。
ただし、外部サービスからのWebhook受け取りや、複雑なHTTPヘッダー制御が必要なREST APIを提供する場合などは、従来のAPI Routesの方が適しています。
Prismaのシングルトン設定を忘れるとどうなりますか?
開発環境でNext.jsのホットリロードが発生するたびに新しいDB接続が作られ、PostgreSQLの最大接続数制限に達してしまいます。"Too many connections" エラーが発生し、開発サーバーが停止したり動作が不安定になります。
ISR(Incremental Static Regeneration)とは何ですか?
静的サイト生成(SSG)のメリット(高速表示)と、サーバーサイドレンダリング(SSR)のメリット(最新データ)を組み合わせた機能です。
「ビルド時にHTMLを生成するが、設定した時間(例:1時間)が経過すると、バックグラウンドでHTMLを再生成する」という挙動になります。
SupabaseとVercel Postgres、どちらが良いですか?
どちらも優秀ですが、Supabaseはフル機能のPostgreSQLを提供し、管理画面も使いやすいため、初心者から中級者には特におすすめです。Vercel PostgresはVercelとの統合が密接で設定が楽ですが、機能面ではSupabaseの方が豊富です。

4 min read · 公開日: 2025年11月24日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事