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.
Next.js là framework React phổ biến nhất hiện nay, hỗ trợ:
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/
├── 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
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.
# 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 "@/*"
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
# 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
// 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>
);
}
// 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>
);
}
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]
useState, useEffect, useRefonClick, onChange)localStorage, window)// 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 });
}
// 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);
}
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 đủ.
# 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
// 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;
}
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.
npm install next-auth@beta
// 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.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"],
};
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
// 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;
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
# Cài đặt shadcn/ui
npx shadcn@latest init
# Thêm components
npx shadcn@latest add button card dialog form input table
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" }
)
);
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"] });
},
});
}
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);
});
});
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.
// 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,
},
};
// 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" }],
},
};
}
// 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,
];
}
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
# 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
# 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.
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.
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
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]
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
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.
Gợi ý tiếp theo: