claude-help

07 - Phát triển Next.js với Claude Code

Hướng dẫn toàn diện để xây dựng ứng dụng Next.js với sự hỗ trợ của Claude Code CLI.


Mục lục

  1. Tổng quan Next.js
  2. App Router vs Pages Router
  3. Khởi tạo dự án
  4. Server Components vs Client Components
  5. API Routes
  6. Tích hợp cơ sở dữ liệu với Prisma
  7. Xác thực với NextAuth.js / Auth.js
  8. Styling với Tailwind CSS
  9. Quản lý State
  10. Testing
  11. Tối ưu SEO
  12. Triển khai
  13. Prompt mẫu cho Next.js

Tổng quan Next.js

Next.js là framework React phổ biến nhất hiện nay, hỗ trợ:


App Router vs Pages Router

App Router (khuyến nghị cho dự án mới)

app/
├── layout.tsx          # Layout gốc
├── page.tsx            # Trang chủ (/)
├── loading.tsx         # UI loading
├── error.tsx           # UI xử lý lỗi
├── not-found.tsx       # Trang 404
├── dashboard/
│   ├── layout.tsx      # Layout cho dashboard
│   ├── page.tsx        # /dashboard
│   └── settings/
│       └── page.tsx    # /dashboard/settings
└── api/
    └── users/
        └── route.ts    # API endpoint

Ưu điểm App Router:

Pages Router (dự án cũ)

pages/
├── index.tsx           # Trang chủ (/)
├── _app.tsx            # App wrapper
├── _document.tsx       # HTML document
├── dashboard/
│   ├── index.tsx       # /dashboard
│   └── settings.tsx    # /dashboard/settings
└── api/
    └── users.ts        # API endpoint

Prompt chọn Router

Tôi đang bắt đầu dự án Next.js mới. Hãy so sánh App Router và Pages Router
cho trường hợp:
- Ứng dụng SaaS với dashboard phức tạp
- Cần SEO tốt cho trang marketing
- Xác thực người dùng
- Real-time notifications

Đề xuất nên dùng Router nào và giải thích lý do.

Khởi tạo dự án

Tạo dự án mới với create-next-app

# Tạo dự án với cấu hình mặc định
npx create-next-app@latest my-app

# Tạo với các tùy chọn cụ thể
npx create-next-app@latest my-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*"

Cấu trúc dự án khuyến nghị

Hãy tạo cấu trúc thư mục cho dự án Next.js 14 App Router với:
- src/ directory
- Tách biệt components, lib, hooks, types, utils
- Prisma cho database
- NextAuth cho xác thực
- Tailwind CSS cho styling

Cấu trúc nên theo best practices và dễ mở rộng.

Claude Code sẽ tạo cấu trúc như sau:

src/
├── app/
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── (dashboard)/
│   │   ├── layout.tsx
│   │   └── dashboard/page.tsx
│   ├── api/
│   │   ├── auth/[...nextauth]/route.ts
│   │   └── users/route.ts
│   ├── layout.tsx
│   ├── page.tsx
│   └── globals.css
├── components/
│   ├── ui/              # Reusable UI components
│   ├── forms/           # Form components
│   └── layouts/         # Layout components
├── lib/
│   ├── prisma.ts        # Prisma client
│   ├── auth.ts          # Auth config
│   └── utils.ts         # Tiện ích
├── hooks/               # Custom hooks
├── types/               # TypeScript types
└── middleware.ts         # Middleware

Thiết lập CLAUDE.md cho dự án Next.js

# Dự án: My App

## Tech Stack
- Next.js 14 (App Router)
- TypeScript
- Tailwind CSS
- Prisma + PostgreSQL
- NextAuth.js v5

## Quy ước
- Dùng Server Components mặc định, chỉ thêm "use client" khi cần
- File naming: kebab-case cho routes, PascalCase cho components
- Luôn dùng TypeScript strict mode
- Tailwind CSS cho styling, không dùng CSS modules

## Commands
- `npm run dev` — Chạy development server
- `npm run build` — Build production
- `npx prisma studio` — Mở Prisma Studio
- `npx prisma migrate dev` — Chạy migration

Server Components vs Client Components

Server Components (mặc định)

// app/users/page.tsx — Server Component (mặc định)
import { prisma } from "@/lib/prisma";

// Có thể truy cập database trực tiếp
export default async function UsersPage() {
  const users = await prisma.user.findMany();

  return (
    <div>
      <h1>Danh sách người dùng</h1>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Client Components

// components/counter.tsx — Client Component
"use client";

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Đếm: {count}
    </button>
  );
}

Prompt phân tích Component

Phân tích component sau và cho biết nó nên là Server Component hay Client Component.
Giải thích lý do và refactor nếu cần:

[dán code component vào đây]

Khi nào dùng Client Component


API Routes

App Router API Routes

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

// Lấy danh sách người dùng
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get("page") || "1");
  const limit = parseInt(searchParams.get("limit") || "10");

  const users = await prisma.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({ users, page, limit });
}

// Tạo người dùng mới
export async function POST(request: NextRequest) {
  const body = await request.json();

  const user = await prisma.user.create({
    data: {
      name: body.name,
      email: body.email,
    },
  });

  return NextResponse.json(user, { status: 201 });
}

Dynamic Route

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await prisma.user.findUnique({
    where: { id: params.id },
  });

  if (!user) {
    return NextResponse.json(
      { error: "Không tìm thấy người dùng" },
      { status: 404 }
    );
  }

  return NextResponse.json(user);
}

Prompt tạo API

Tạo CRUD API routes cho entity "Product" trong Next.js App Router với:
- GET /api/products — Danh sách có phân trang, tìm kiếm, lọc theo category
- GET /api/products/[id] — Chi tiết sản phẩm
- POST /api/products — Tạo mới (có validation với Zod)
- PUT /api/products/[id] — Cập nhật
- DELETE /api/products/[id] — Xóa (soft delete)

Sử dụng Prisma ORM, có error handling đầy đủ.

Tích hợp cơ sở dữ liệu với Prisma

Cài đặt Prisma

# Cài đặt Prisma
npm install prisma @prisma/client
npx prisma init

# Sau khi định nghĩa schema
npx prisma migrate dev --name init
npx prisma generate

Prisma Client singleton

// lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query"] : [],
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

Prompt thiết kế schema

Thiết kế Prisma schema cho ứng dụng e-commerce với:
- User (có role: ADMIN, CUSTOMER)
- Product (có category, variants, images)
- Order (có trạng thái, items, shipping address)
- Review (đánh giá sản phẩm)
- Cart (giỏ hàng)

Thêm indexes phù hợp, relations, và soft delete cho các bảng chính.

Xem thêm chi tiết tại 08-database-postgresql.md.


Xác thực với NextAuth.js / Auth.js

Cài đặt Auth.js v5

npm install next-auth@beta

Cấu hình Auth.js

// lib/auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHub,
    Google,
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Mật khẩu", type: "password" },
      },
      async authorize(credentials) {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });

        if (!user || !user.password) return null;

        const isValid = await bcrypt.compare(
          credentials.password as string,
          user.password
        );

        return isValid ? user : null;
      },
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      if (token.sub) {
        session.user.id = token.sub;
      }
      return session;
    },
  },
});

Middleware bảo vệ routes

// middleware.ts
import { auth } from "@/lib/auth";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isAuthPage = req.nextUrl.pathname.startsWith("/login");
  const isProtected = req.nextUrl.pathname.startsWith("/dashboard");

  if (isProtected && !isLoggedIn) {
    return Response.redirect(new URL("/login", req.nextUrl));
  }

  if (isAuthPage && isLoggedIn) {
    return Response.redirect(new URL("/dashboard", req.nextUrl));
  }
});

export const config = {
  matcher: ["/dashboard/:path*", "/login", "/register"],
};

Prompt thiết lập Auth

Thiết lập NextAuth.js v5 (Auth.js) cho dự án Next.js 14 App Router với:
- Đăng nhập bằng email/password
- Đăng nhập bằng Google OAuth
- Prisma adapter với PostgreSQL
- Middleware bảo vệ routes /dashboard/*
- Session callback thêm user role
- Trang đăng nhập và đăng ký tùy chỉnh

Styling với Tailwind CSS

Cấu hình Tailwind

// tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: "#eff6ff",
          500: "#3b82f6",
          600: "#2563eb",
          700: "#1d4ed8",
        },
      },
    },
  },
  plugins: [
    require("@tailwindcss/forms"),
    require("@tailwindcss/typography"),
  ],
};

export default config;

Prompt tạo UI Component

Tạo component Card cho dashboard với Tailwind CSS:
- Hiển thị tiêu đề, giá trị, icon, phần trăm thay đổi
- Có animation khi hover
- Responsive (4 cột desktop, 2 tablet, 1 mobile)
- Dark mode support
- Dùng TypeScript với props interface rõ ràng

Tích hợp shadcn/ui

# Cài đặt shadcn/ui
npx shadcn@latest init

# Thêm components
npx shadcn@latest add button card dialog form input table

Quản lý State

Zustand (khuyến nghị)

npm install zustand
// stores/cart-store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
  totalPrice: () => number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
      addItem: (item) =>
        set((state) => {
          const existing = state.items.find((i) => i.id === item.id);
          if (existing) {
            return {
              items: state.items.map((i) =>
                i.id === item.id
                  ? { ...i, quantity: i.quantity + 1 }
                  : i
              ),
            };
          }
          return { items: [...state.items, { ...item, quantity: 1 }] };
        }),
      removeItem: (id) =>
        set((state) => ({
          items: state.items.filter((i) => i.id !== id),
        })),
      clearCart: () => set({ items: [] }),
      totalPrice: () =>
        get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
    }),
    { name: "cart-storage" }
  )
);

Server State với TanStack Query

npm install @tanstack/react-query
// hooks/use-products.ts
"use client";

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

export function useProducts(page: number = 1) {
  return useQuery({
    queryKey: ["products", page],
    queryFn: () =>
      fetch(`/api/products?page=${page}`).then((r) => r.json()),
  });
}

export function useCreateProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateProductInput) =>
      fetch("/api/products", {
        method: "POST",
        body: JSON.stringify(data),
      }).then((r) => r.json()),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["products"] });
    },
  });
}

Testing

Jest cho Unit Testing

npm install -D jest @testing-library/react @testing-library/jest-dom
npm install -D jest-environment-jsdom @types/jest ts-jest
// __tests__/components/button.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { Button } from "@/components/ui/button";

describe("Button component", () => {
  it("hiển thị text đúng", () => {
    render(<Button>Bấm vào đây</Button>);
    expect(screen.getByText("Bấm vào đây")).toBeInTheDocument();
  });

  it("gọi onClick khi bấm", () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Bấm</Button>);
    fireEvent.click(screen.getByText("Bấm"));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

Playwright cho E2E Testing

npm install -D @playwright/test
npx playwright install
// e2e/login.spec.ts
import { test, expect } from "@playwright/test";

test("đăng nhập thành công", async ({ page }) => {
  await page.goto("/login");
  await page.fill('input[name="email"]', "user@example.com");
  await page.fill('input[name="password"]', "password123");
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL("/dashboard");
  await expect(page.locator("h1")).toContainText("Dashboard");
});

Xem thêm chi tiết tại 09-testing-va-qa.md.


Tối ưu SEO

Metadata API

// app/layout.tsx
import { Metadata } from "next";

export const metadata: Metadata = {
  title: {
    default: "Tên ứng dụng",
    template: "%s | Tên ứng dụng",
  },
  description: "Mô tả ứng dụng của bạn",
  openGraph: {
    title: "Tên ứng dụng",
    description: "Mô tả ứng dụng",
    url: "https://example.com",
    siteName: "Tên ứng dụng",
    images: [
      {
        url: "/og-image.png",
        width: 1200,
        height: 630,
      },
    ],
    locale: "vi_VN",
    type: "website",
  },
  robots: {
    index: true,
    follow: true,
  },
};

Dynamic Metadata

// app/products/[id]/page.tsx
import { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  const product = await prisma.product.findUnique({
    where: { id: params.id },
  });

  return {
    title: product?.name,
    description: product?.description,
    openGraph: {
      images: [{ url: product?.image || "/default.png" }],
    },
  };
}

Sitemap và Robots

// app/sitemap.ts
import { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const products = await prisma.product.findMany({
    select: { id: true, updatedAt: true },
  });

  const productUrls = products.map((product) => ({
    url: `https://example.com/products/${product.id}`,
    lastModified: product.updatedAt,
    changeFrequency: "weekly" as const,
    priority: 0.8,
  }));

  return [
    {
      url: "https://example.com",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 1,
    },
    ...productUrls,
  ];
}

Prompt SEO

Tối ưu SEO cho trang sản phẩm trong Next.js App Router:
- Metadata động từ database
- Structured data (JSON-LD) cho sản phẩm
- Open Graph và Twitter Card
- Sitemap tự động
- Canonical URLs
- Breadcrumbs schema

Triển khai

Vercel (khuyến nghị cho Next.js)

# Cài đặt Vercel CLI
npm install -g vercel

# Triển khai
vercel

# Triển khai production
vercel --prod

# Thiết lập biến môi trường
vercel env add DATABASE_URL
vercel env add NEXTAUTH_SECRET

Docker (tự host)

# Dockerfile
FROM node:20-alpine AS base

FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
// next.config.js — bật standalone output
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
};

module.exports = nextConfig;

Xem thêm chi tiết tại 11-deploy-production.md.


Prompt mẫu cho Next.js

Tạo dự án từ đầu

Tạo ứng dụng quản lý dự án (Project Management) với Next.js 14:

Tech stack:
- Next.js 14 App Router + TypeScript
- Prisma + PostgreSQL
- NextAuth.js v5 (email/password + Google)
- Tailwind CSS + shadcn/ui
- Zustand cho state management
- TanStack Query cho server state

Tính năng:
1. Đăng nhập/Đăng ký
2. Dashboard tổng quan
3. CRUD dự án
4. Quản lý task (Kanban board)
5. Mời thành viên vào dự án
6. Bình luận trong task
7. Thông báo real-time

Hãy bắt đầu với cấu trúc dự án và Prisma schema.

Tối ưu hiệu suất

Phân tích và tối ưu hiệu suất cho trang danh sách sản phẩm:
- Hiện đang load chậm (> 3 giây)
- Có 500+ sản phẩm với hình ảnh
- Cần phân trang, lọc, tìm kiếm

Áp dụng:
- Image optimization với next/image
- Dynamic imports và code splitting
- Caching strategy
- Database query optimization
- Loading UI với Suspense

Refactor sang App Router

Chuyển đổi trang sau từ Pages Router sang App Router:
- Giữ nguyên chức năng
- Tận dụng Server Components khi có thể
- Thêm loading.tsx và error.tsx
- Cập nhật data fetching từ getServerSideProps sang async component

[dán code trang Pages Router vào đây]

Xử lý form phức tạp

Tạo form tạo sản phẩm với:
- React Hook Form + Zod validation
- Upload nhiều hình ảnh (drag & drop)
- Rich text editor cho mô tả
- Dynamic fields cho variants (size, color, price)
- Preview trước khi submit
- Server Action để xử lý form
- Hiển thị lỗi validation rõ ràng

Server Actions

Tạo Server Actions cho chức năng giỏ hàng:
- addToCart(productId, quantity)
- removeFromCart(itemId)
- updateQuantity(itemId, quantity)
- checkout()

Sử dụng Prisma, có validation, error handling, và revalidation.

Tài nguyên tham khảo


Gợi ý tiếp theo: