取り組みの背景
Next.js は React ベースのフルスタック Web フレームワークとして、サーバーサイドレンダリング (SSR)、静的生成 (SSG)、API ルート、そして最新の App Router など、モダンな Web 開発に必要な機能を備えています。
Antigravity と組み合わせることで、Next.js の開発効率をさらに飛躍的に向上させることができます。自動コンポーネント生成、API ロジック実装の自動化、テストコード生成など、開発の各段階での効率化が可能です。
ここでAntigravity を活用した Next.js プロジェクトの構築方法を、セットアップからデプロイまで、実践的に解説します。
Next.js + Antigravity プロジェクトの初期設定
プロジェクト構造の計画
# Antigravity 用にプロジェクト構造を最適化した Next.js プロジェクト作成
my-app/
├── app/ # App Router (メインアプリケーション)
│ ├── layout.tsx # ルートレイアウト
│ ├── page.tsx # ホームページ
│ ├── api/ # API ルート
│ │ ├── users/
│ │ ├── posts/
│ │ └── auth/
│ └── dashboard/ # 認証済みユーザー向けページ
├── components/ # 再利用可能なコンポーネント
│ ├── ui/ # UI コンポーネント (Button, Card, etc)
│ ├── layouts/ # レイアウトコンポーネント
│ └── features/ # 機能別コンポーネント
├── lib/ # ユーティリティと設定
│ ├── db.ts # データベース接続
│ ├── auth.ts # 認証ロジック
│ ├── utils.ts # ヘルパー関数
│ └── types.ts # TypeScript 型定義
├── styles/ # グローバルスタイル
├── public/ # 静的ファイル
├── prisma/ # ORM スキーマ
├── .env.local # 環境変数
└── next.config.js # Next.js 設定プロジェクト作成と依存関係インストール
# Next.js プロジェクトを作成
npx create-next-app@latest my-app --typescript --tailwind --eslint
# 必要なライブラリをインストール
npm install prisma @prisma/client
npm install bcryptjs jsonwebtoken
npm install axios zod
npm install -D @types/node @types/reactAntigravity との統合設定
Antigravity でコード生成を効率化するために、プロンプトテンプレートを設定します:
# Antigravity System Prompt for Next.js Development
あなたは Next.js フルスタック開発の専門家です。
以下のベストプラクティスに従ってください:
## コンポーネント生成
- React Server Components (RSC) を優先的に使用
- "use client" ディレクティブは対話的なコンポーネントのみに使用
- TypeScript で型を明確に定義
- Tailwind CSS でスタイルを管理
## API ルート実装
- 入力検証には Zod を使用
- エラーハンドリングは try-catch で適切に
- 認証チェックを実装
- レスポンスは JSON 形式で統一
## データベース
- Prisma ORM を使用
- マイグレーションは自動管理
- 関連する型情報を含める
## セキュリティ
- 環境変数で機密情報を管理
- CSRF 対策を実装
- 入力値の検証を厳密にApp Router を使用したプロジェクト構築
レイアウト構造の実装
// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import '@/styles/globals.css';
import Navbar from '@/components/layouts/Navbar';
import Footer from '@/components/layouts/Footer';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'My App',
description: 'Generated with Antigravity + Next.js',
icons: {
icon: '/favicon.ico',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className={inter.className}>
<Navbar />
<main className="min-h-screen">
{children}
</main>
<Footer />
</body>
</html>
);
}ホームページの実装
// app/page.tsx
import Link from 'next/link';
import Button from '@/components/ui/Button';
export default function Home() {
return (
<div className="container mx-auto px-4 py-16">
<div className="max-w-2xl mx-auto text-center">
<h1 className="text-5xl font-bold mb-6">
Welcome to My App
</h1>
<p className="text-xl text-gray-600 mb-8">
Powered by Antigravity + Next.js
</p>
<div className="flex gap-4 justify-center">
<Link href="/about">
<Button variant="primary">Learn More</Button>
</Link>
<Link href="/dashboard">
<Button variant="outline">Dashboard</Button>
</Link>
</div>
</div>
</div>
);
}Antigravity でのコンポーネント生成
UI コンポーネントの自動生成
Antigravity プロンプト例:
次の UI コンポーネントを TypeScript で実装してください:
コンポーネント: BlogCard
説明: ブログ記事を表示するカードコンポーネント
仕様:
- Props: title, excerpt, author, publishDate, imageUrl, slug
- スタイル: Tailwind CSS で実装
- ホバーエフェクト: スケールアップとシャドウ追加
- リンク: 記事詳細ページへのリンク含む
- レスポンシブ: モバイル対応
出力形式: 完全な React コンポーネントコード生成されたコンポーネントの例
// components/ui/BlogCard.tsx
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { formatDate } from '@/lib/utils';
interface BlogCardProps {
title: string;
excerpt: string;
author: string;
publishDate: Date;
imageUrl: string;
slug: string;
}
export default function BlogCard({
title,
excerpt,
author,
publishDate,
imageUrl,
slug,
}: BlogCardProps) {
return (
<Link href={`/blog/${slug}`}>
<article className="group cursor-pointer">
<div className="relative overflow-hidden rounded-lg mb-4 bg-gray-100">
<Image
src={imageUrl}
alt={title}
width={400}
height={250}
className="w-full h-[250px] object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div className="group-hover:shadow-lg transition-shadow p-4 -mx-4">
<h3 className="text-xl font-bold mb-2 group-hover:text-blue-600">
{title}
</h3>
<p className="text-gray-600 mb-3 line-clamp-2">
{excerpt}
</p>
<div className="flex justify-between text-sm text-gray-500">
<span>{author}</span>
<span>{formatDate(publishDate)}</span>
</div>
</div>
</article>
</Link>
);
}API ルートの実装
ユーザー API の実装例
Antigravity プロンプト:
Next.js API ルート を実装してください
機能: ユーザー一覧取得 API
仕様:
- エンドポイント: GET /api/users
- 認証: JWT トークン必須
- クエリパラメータ: page=1, limit=10
- レスポンス: ユーザーオブジェクトの配列
- エラーハンドリング: 認証失敗は 401, サーバーエラーは 500
- バリデーション: Zod で入力値検証実装コード
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { verifyAuth } from '@/lib/auth';
import { db } from '@/lib/db';
// クエリパラメータの検証スキーマ
const querySchema = z.object({
page: z.string().regex(/^\d+$/).optional().default('1'),
limit: z.string().regex(/^\d+$/).optional().default('10'),
});
export async function GET(request: NextRequest) {
try {
// 認証チェック
const user = await verifyAuth(request);
if (!user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// クエリパラメータの検証
const url = new URL(request.url);
const query = querySchema.parse({
page: url.searchParams.get('page'),
limit: url.searchParams.get('limit'),
});
const page = parseInt(query.page);
const limit = parseInt(query.limit);
const skip = (page - 1) * limit;
// データベースクエリ
const [users, total] = await Promise.all([
db.user.findMany({
skip,
take: limit,
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
}),
db.user.count(),
]);
return NextResponse.json({
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('Users API error:', error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid parameters', details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}認証の実装
JWT ベースの認証システム
// lib/auth.ts
import jwt from 'jsonwebtoken';
import bcryptjs from 'bcryptjs';
import { NextRequest } from 'next/server';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export interface AuthPayload {
userId: string;
email: string;
}
export function generateToken(payload: AuthPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
}
export function verifyToken(token: string): AuthPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as AuthPayload;
} catch {
return null;
}
}
export async function verifyAuth(request: NextRequest): Promise<AuthPayload | null> {
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return null;
}
const token = authHeader.slice(7);
return verifyToken(token);
}
export async function hashPassword(password: string): Promise<string> {
return bcryptjs.hash(password, 10);
}
export async function comparePasswords(
password: string,
hash: string
): Promise<boolean> {
return bcryptjs.compare(password, hash);
}ログイン API
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { generateToken, comparePasswords } from '@/lib/auth';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, password } = loginSchema.parse(body);
const user = await db.user.findUnique({
where: { email },
});
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
const isValidPassword = await comparePasswords(password, user.passwordHash);
if (!isValidPassword) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
const token = generateToken({
userId: user.id,
email: user.email,
});
return NextResponse.json({
token,
user: {
id: user.id,
name: user.name,
email: user.email,
},
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Tailwind CSS でのスタイリング
グローバルスタイルと Tailwind 設定
/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply scroll-smooth;
}
body {
@apply bg-white dark:bg-slate-950 text-gray-900 dark:text-gray-50;
}
}
@layer components {
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-colors;
}
.btn-primary {
@apply bg-blue-600 text-white hover:bg-blue-700;
}
.btn-outline {
@apply border border-gray-300 text-gray-700 hover:bg-gray-50;
}
.container-tight {
@apply max-w-4xl mx-auto px-4;
}
}Tailwind 設定ファイル
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
brand: {
50: '#f5f3ff',
600: '#7c3aed',
900: '#3e1d56',
},
},
fontFamily: {
sans: ['var(--font-inter)', 'system-ui'],
},
},
},
plugins: [],
};Cloudflare/Vercel へのデプロイ
Vercel へのデプロイ
# Vercel CLI のインストール
npm install -g vercel
# プロジェクトをデプロイ
vercel
# 本番環境にデプロイ
vercel --prod環境変数の設定
# .env.local(開発環境)
DATABASE_URL="postgresql://user:password@localhost:5432/myapp"
JWT_SECRET="your-secret-key"
NEXTAUTH_SECRET="another-secret"
# Vercel ダッシュボードで本番環境変数を設定
# Project Settings > Environment VariablesCloudflare Pages へのデプロイ
# Wrangler CLI をインストール
npm install -g wrangler
# Cloudflare アカウントでログイン
wrangler login
# デプロイ
wrangler pages deploy ./outパフォーマンス最適化
画像最適化
// components/OptimizedImage.tsx
import Image from 'next/image';
interface OptimizedImageProps {
src: string;
alt: string;
width: number;
height: number;
}
export default function OptimizedImage({
src,
alt,
width,
height,
}: OptimizedImageProps) {
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={false}
loading="lazy"
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
/>
);
}API キャッシング
// lib/cache.ts
export async function getCachedData<T>(
key: string,
fetchFn: () => Promise<T>,
ttl: number = 3600
): Promise<T> {
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const data = await fetchFn();
await redis.set(key, JSON.stringify(data), 'EX', ttl);
return data;
}テストの実装
API テストの例
// __tests__/api/users.test.ts
import { GET } from '@/app/api/users/route';
import { NextRequest } from 'next/server';
describe('GET /api/users', () => {
it('should return 401 without auth', async () => {
const request = new NextRequest('http://localhost:3000/api/users');
const response = await GET(request);
expect(response.status).toBe(401);
});
it('should return users with valid auth', async () => {
const request = new NextRequest('http://localhost:3000/api/users', {
headers: {
'Authorization': 'Bearer valid-token',
},
});
const response = await GET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toHaveProperty('users');
expect(Array.isArray(data.users)).toBe(true);
});
});ベストプラクティス
1. Server Components の活用
// app/posts/page.tsx
import { db } from '@/lib/db';
import PostCard from '@/components/PostCard';
// Server Component: サーバーでのみ実行
export default async function PostsPage() {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
});
return (
<div className="grid gap-6">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}2. 環境変数の管理
# .env.local
DATABASE_URL=
JWT_SECRET=
NEXTAUTH_SECRET=
# .env.production
DATABASE_URL=
JWT_SECRET=3. エラーハンドリング
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-2xl font-bold mb-4">Something went wrong</h1>
<p className="text-gray-600 mb-6">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Try again
</button>
</div>
);
}まとめ
Antigravity と Next.js の組み合わせは、モダン Web アプリケーション開発を大幅に加速させます。
主なメリット:
- 自動コンポーネント生成で開発時間を削減
- API ロジックの自動生成で実装の効率化
- TypeScript による型安全性の確保
- テストコード生成で品質向上
これらのテクニックを活用することで、高速で保守性の高い Next.js アプリケーションを構築できます。
個人開発12年の現場で実感したこと
線引きするときの3つの判断軸
- 失敗時の影響が金銭やユーザー体験にどれだけ波及するか
- 復旧オペレーションが明文化されているか
- 観測ログから人間が再現できる粒度に整っているか