Why Next.js?
Next.js has become the go-to framework for building modern web applications, and for good reason:
- Server-side rendering (SSR) and static site generation (SSG)
- Exceptional performance out of the box
- Built-in routing and API capabilities
- Excellent developer experience
- Vercel deployment integration
With the introduction of the App Router in Next.js 13 and refined in version 14, the framework has reached new levels of power and flexibility.
Essential Best Practices
1. Leverage Server Components
Server Components are a game-changer. By default, all components in the App Router are Server Components, which means:
// This runs on the server by default
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{/* Render data */}</div>;
}
Benefits:
- Zero JavaScript sent to the client for these components
- Direct database access without API layers
- Improved performance and SEO
2. Use Client Components Wisely
Only opt into Client Components when you need:
- Browser APIs
- Event handlers
- React hooks (useState, useEffect, etc.)
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
3. Optimize Images
Always use Next.js's Image component:
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // for above-the-fold images
/>
4. Implement Proper Loading States
Use React Suspense and Next.js loading files:
// app/dashboard/loading.tsx
export default function Loading() {
return <LoadingSkeleton />;
}
5. Error Handling
Create error boundaries for graceful error handling:
// app/dashboard/error.tsx
'use client';
export default function Error({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Performance Optimization
Route Segment Configuration
Optimize how pages are rendered:
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// Revalidate every hour
export const revalidate = 3600;
// Static rendering
export const dynamic = 'force-static';
Parallel Routes and Intercepting Routes
For complex layouts, use parallel routes:
app/
@analytics/
@team/
layout.tsx
Streaming
Stream content as it's ready:
import { Suspense } from 'react';
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<LoadingSkeleton />}>
<SlowComponent />
</Suspense>
<Footer />
</>
);
}
TypeScript Best Practices
Type Your Props Properly
interface PageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default function Page({ params, searchParams }: PageProps) {
// Fully typed!
}
Use Zod for Runtime Validation
import { z } from 'zod';
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18),
});
type User = z.infer<typeof UserSchema>;
Data Fetching Patterns
Server-Side Data Fetching
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (!res.ok) throw new Error('Failed to fetch data');
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{/* Use data */}</div>;
}
Parallel Data Fetching
export default async function Page() {
const [users, posts] = await Promise.all([
getUsers(),
getPosts(),
]);
return <div>{/* Render users and posts */}</div>;
}
SEO Optimization
Metadata API
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Page',
description: 'Page description',
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
};
Dynamic Metadata
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await getProduct(params.id);
return {
title: product.title,
description: product.description,
};
}
Testing
Unit Testing with Jest
import { render, screen } from '@testing-library/react';
import Page from './page';
test('renders page', () => {
render(<Page />);
expect(screen.getByText('Hello World')).toBeInTheDocument();
});
E2E Testing with Playwright
import { test, expect } from '@playwright/test';
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toContainText('Welcome');
});
Deployment Best Practices
Environment Variables
# .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
Vercel Deployment
For optimal performance on Vercel:
- Use Edge Runtime for dynamic routes when possible
- Configure proper caching headers
- Enable analytics and speed insights
- Use Vercel's image optimization
Common Pitfalls to Avoid
- Don't use 'use client' everywhere - Start with Server Components
- Avoid client-side data fetching for initial render - Use Server Components
- Don't ignore loading and error states - Always provide good UX
- Don't skip image optimization - Use the Image component
- Avoid prop drilling - Use React Context or state management
Conclusion
Next.js 14 with the App Router provides an incredible foundation for building modern web applications. By following these best practices, you'll create applications that are:
- Fast and performant
- SEO-friendly
- Maintainable and scalable
- Delightful for users
At Hampton.io, we use these practices in all our Next.js projects, delivering high-quality applications for our clients.
Need help with your Next.js project? Let's chat!