Master Next.js for building modern full-stack applications. Learn about server components, data fetching, API routes, and deployment strategies. 🚀
Server Components 🎯
// app/page.tsx
import { Suspense } from 'react';
import { getProducts } from '@/lib/products';
async function ProductList() {
const products = await getProducts();
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<div key={product.id} className="p-4 border">
<h2>{product.name}</h2>
<p>{product.description}</p>
<span>${product.price}</span>
</div>
))}
</div>
);
}
export default function HomePage() {
return (
<main className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-4">
Products
</h1>
<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>
</main>
);
}
// Loading UI
function ProductsSkeleton() {
return (
<div className="grid grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="animate-pulse p-4 border"
>
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2 mt-2" />
<div className="h-4 bg-gray-200 rounded w-1/4 mt-2" />
</div>
))}
</div>
);
}
Data Fetching 🔄
// lib/products.ts
import { sql } from '@vercel/postgres';
import { unstable_cache } from 'next/cache';
export interface Product {
id: string;
name: string;
description: string;
price: number;
createdAt: Date;
}
export const getProducts = unstable_cache(
async () => {
const { rows } = await sql<Product>`
SELECT *
FROM products
ORDER BY created_at DESC
LIMIT 100
`;
return rows;
},
['products'],
{
revalidate: 60, // Cache for 1 minute
tags: ['products']
}
);
export const getProduct = unstable_cache(
async (id: string) => {
const { rows } = await sql<Product>`
SELECT *
FROM products
WHERE id = ${id}
`;
return rows[0];
},
['product'],
{
revalidate: 60,
tags: ['product']
}
);
// app/products/[id]/page.tsx
import { getProduct } from '@/lib/products';
import { notFound } from 'next/navigation';
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-4">
{product.name}
</h1>
<p className="text-gray-600 mb-4">
{product.description}
</p>
<span className="text-2xl font-bold">
${product.price}
</span>
</div>
);
}
API Routes 📡
// app/api/products/route.ts
import { sql } from '@vercel/postgres';
import { revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';
import { z } from 'zod';
const productSchema = z.object({
name: z.string().min(1),
description: z.string().min(1),
price: z.number().positive()
});
export async function POST(request: Request) {
try {
const json = await request.json();
const body = productSchema.parse(json);
const { rows } = await sql`
INSERT INTO products (name, description, price)
VALUES (${body.name}, ${body.description}, ${body.price})
RETURNING *
`;
revalidateTag('products');
return NextResponse.json(rows[0], { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') ?? '1');
const limit = parseInt(searchParams.get('limit') ?? '10');
const offset = (page - 1) * limit;
try {
const { rows } = await sql`
SELECT *
FROM products
ORDER BY created_at DESC
LIMIT ${limit}
OFFSET ${offset}
`;
return NextResponse.json(rows);
} catch (error) {
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
// app/api/products/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { rows } = await sql`
SELECT *
FROM products
WHERE id = ${params.id}
`;
if (!rows[0]) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
return NextResponse.json(rows[0]);
} catch (error) {
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const json = await request.json();
const body = productSchema.parse(json);
const { rows } = await sql`
UPDATE products
SET name = ${body.name},
description = ${body.description},
price = ${body.price}
WHERE id = ${params.id}
RETURNING *
`;
if (!rows[0]) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
revalidateTag('products');
revalidateTag('product');
return NextResponse.json(rows[0]);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { rows } = await sql`
DELETE FROM products
WHERE id = ${params.id}
RETURNING *
`;
if (!rows[0]) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
revalidateTag('products');
revalidateTag('product');
return NextResponse.json(rows[0]);
} catch (error) {
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
Client Components 💻
// components/ProductForm.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { z } from 'zod';
const productSchema = z.object({
name: z.string().min(1),
description: z.string().min(1),
price: z.number().positive()
});
export function ProductForm() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function onSubmit(
event: React.FormEvent<HTMLFormElement>
) {
event.preventDefault();
setError(null);
const formData = new FormData(event.currentTarget);
const rawData = {
name: formData.get('name'),
description: formData.get('description'),
price: parseFloat(formData.get('price') as string)
};
try {
const data = productSchema.parse(rawData);
const response = await fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Failed to create product');
}
router.refresh();
router.push('/products');
} catch (error) {
if (error instanceof z.ZodError) {
setError(error.errors[0].message);
} else {
setError('Something went wrong');
}
}
}
return (
<form onSubmit={onSubmit} className="space-y-4">
{error && (
<p className="text-red-500">{error}</p>
)}
<div>
<label htmlFor="name" className="block">
Name
</label>
<input
type="text"
id="name"
name="name"
className="border p-2 w-full"
required
/>
</div>
<div>
<label htmlFor="description" className="block">
Description
</label>
<textarea
id="description"
name="description"
className="border p-2 w-full"
required
/>
</div>
<div>
<label htmlFor="price" className="block">
Price
</label>
<input
type="number"
id="price"
name="price"
step="0.01"
className="border p-2 w-full"
required
/>
</div>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Create Product
</button>
</form>
);
}
Authentication 🔒
// lib/auth.ts
import { sql } from '@vercel/postgres';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import NextAuth from 'next-auth';
import { AuthOptions } from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const authOptions: AuthOptions = {
adapter: DrizzleAdapter(sql),
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!
})
],
callbacks: {
async session({ session, user }) {
if (session.user) {
session.user.id = user.id;
}
return session;
}
}
};
export const {
handlers: { GET, POST },
auth,
signIn,
signOut
} = NextAuth(authOptions);
// middleware.ts
import { auth } from '@/lib/auth';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const session = await auth();
if (!session) {
return NextResponse.redirect(
new URL('/api/auth/signin', request.url)
);
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*']
};
Deployment 🚀
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
serverComponentsExternalPackages: ['@prisma/client']
},
images: {
domains: ['images.example.com']
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
}
]
}
];
}
};
module.exports = nextConfig;
// package.json
{
"name": "next-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"migrate": "prisma migrate deploy"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.0.0",
"@prisma/client": "^5.0.0",
"@vercel/postgres": "^0.5.0",
"next": "14.0.0",
"next-auth": "^5.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"autoprefixer": "^10.0.0",
"eslint": "^8.0.0",
"eslint-config-next": "14.0.0",
"jest": "^29.0.0",
"postcss": "^8.0.0",
"prisma": "^5.0.0",
"tailwindcss": "^3.0.0",
"typescript": "^5.0.0"
}
}
Best Practices 📝
- Use server components
- Implement proper caching
- Use TypeScript
- Implement proper authentication
- Use proper data validation
- Implement proper error handling
- Use proper deployment strategies
- Implement proper testing
- Use proper security measures
- Follow Next.js best practices
Additional Resources
Next.js provides a powerful framework for building modern full-stack applications. This guide covers essential concepts and best practices for developing with Next.js.