NextJS Tutorial - 12 Essential Concepts You Need to Know
This guide represents my journey of learning Next.js, compiled from various sources including ByteGrad's tutorial, official documentation, and practical experience. As I was learning these concepts, I organized them into this comprehensive guide to serve as both a reference for myself and a resource for others. Whether you're new to Next.js or looking to enhance your web applications, this guide provides practical examples and explanations that helped me understand and implement Next.js effectively.
1. Setting Up a New Next.js Project
Next.js is a React framework that enables features like server-side rendering, static site generation, and API routes. Setting up a new project is straightforward with the create-next-app
tool.
Fresh Install
# Create a new project in a new folder
npx create-next-app@latest my-nextjs-app
# Create a project in the current directory
npx create-next-app@latest .
During installation, you'll be prompted with several configuration options:
What is your project named? my-nextjs-app
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like your code inside a `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to use Turbopack for `next dev`? No
Would you like to customize the import alias (@/*)? No
After installation, you can start the development server:
npm run dev
2. Routing & Navigation
Next.js uses file-system based routing within the app
directory.
Page Structure
src/
└── app/
├── page.tsx # Home page (/)
├── posts/
│ ├── page.tsx # Posts page (/posts)
│ └── [id]/
│ └── page.tsx # Dynamic route (/posts/1, /posts/2, etc.)
└── layout.tsx # Root layout wrapping all pages
Navigation Between Pages
// Header component example
import Link from 'next/link';
import Image from 'next/image';
export default function Header() {
return (
<header>
<Link href="/">
<Image
src="/logo.png"
alt="Logo"
width={100}
height={50}
/>
</Link>
<nav>
<Link href="/">Home</Link>
<Link href="/posts">Posts</Link>
</nav>
</header>
);
}
3. Metadata
Next.js provides a straightforward way to manage metadata for SEO and social sharing.
Adding Metadata
// In layout.tsx (applies to all pages)
export const metadata = {
title: 'My Next.js App',
description: 'A full-stack application built with Next.js',
openGraph: {
title: 'My Next.js App',
description: 'A full-stack application built with Next.js',
images: [
{
url: 'https://example.com/og.png',
width: 800,
height: 600,
}
],
}
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Page-Specific Metadata
// In posts/page.tsx (overrides root metadata)
export const metadata = {
title: 'All Posts | My Next.js App',
description: 'Browse all blog posts',
};
export default function PostsPage() {
// Page component
}
4. Styling with Tailwind CSS
Tailwind CSS comes integrated with Next.js by default, providing utility classes for styling.
Using Tailwind Classes
// Example page using Tailwind CSS
export default function HomePage() {
return (
<div className="max-w-4xl mx-auto p-4">
<h1 className="text-3xl font-bold mb-6 text-blue-600">
My Store
</h1>
<p className="text-gray-700">
Welcome to my Next.js application styled with Tailwind CSS.
</p>
<button className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Learn More
</button>
</div>
);
}
5. Image Component
Next.js provides an optimized Image component that automatically handles responsive sizes, modern formats, and lazy loading.
Using the Image Component
import Image from 'next/image';
export default function ProductCard() {
return (
<div className="border rounded-lg p-4">
<Image
src="/products/headphones.jpg"
alt="Headphones"
width={400}
height={300}
priority // loads immediately, no lazy loading
className="rounded-md"
/>
{/* For external images, you must configure domains */}
<Image
src="https://external-domain.com/image.jpg"
alt="External image"
width={400}
height={300}
/>
</div>
);
}
Configuring External Domains
In next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['external-domain.com'],
},
};
module.exports = nextConfig;
6. Client vs Server Components
Next.js 13+ introduces a fundamental distinction between Server and Client Components.
Server Components (Default)
Server Components run only on the server and can:
- Fetch data directly without useEffect
- Access server-only resources (databases, file system)
- Keep large dependencies on the server
// Server Component (default in app/ directory)
async function ProductComponent() {
// Direct data fetching without useEffect
const response = await fetch('https://dummyjson.com/products/3');
const product = await response.json();
return (
<div>
<h2>{product.title}</h2>
<p>{product.description}</p>
</div>
);
}
export default function Page() {
return (
<main>
<h1>My Store</h1>
{/* Server component can be used directly */}
<ProductComponent />
</main>
);
}
Client Components
Client Components run on both server (for initial HTML) and client (for interactivity).
'use client'; // This directive marks it as a client component
import { useState } from 'react';
export default function FavoriteButton() {
const [isFavorite, setIsFavorite] = useState(false);
// Safe browser API usage
const hasBeenFavoritedBefore = typeof window !== 'undefined'
? localStorage.getItem('favorite') === 'true'
: false;
return (
<button
onClick={() => {
setIsFavorite(!isFavorite);
if (typeof window !== 'undefined') {
localStorage.setItem('favorite', (!isFavorite).toString());
}
}}
className={`p-2 rounded ${isFavorite ? 'bg-red-500' : 'bg-gray-200'}`}
>
{isFavorite ? '❤️' : '♡'}
{hasBeenFavoritedBefore ? 'You have favorited this before' : 'No'}
</button>
);
}
7. Server Actions
Server Actions allow you to define server-side functions that can be called directly from client components, perfect for form submissions.
Creating a Server Action
// In posts/page.tsx
import { revalidatePath } from 'next/cache';
import { prisma } from '@/lib/prisma'; // Assuming you have Prisma setup
// Server action to add a new post
async function addPost(formData) {
'use server';
// Extract form data
const title = formData.get('title');
const body = formData.get('body');
// Simulate delay for demonstration
await new Promise(resolve => setTimeout(resolve, 2000));
// Add to database
await prisma.post.create({
data: {
title,
body,
},
});
// Refresh the current path to show new data
revalidatePath('/posts');
}
// Form component with loading state
function AddPostForm() {
'use client';
const { pending } = useFormStatus();
return (
<form action={addPost}>
<input
name="title"
placeholder="Title"
className="border p-2 mb-2 block w-full"
/>
<textarea
name="body"
placeholder="Content"
className="border p-2 mb-2 block w-full h-24"
></textarea>
<button
type="submit"
disabled={pending}
className={`p-2 bg-blue-500 text-white ${pending ? 'opacity-50' : ''}`}
>
{pending ? 'Submitting...' : 'Submit New Post'}
</button>
</form>
);
}
export default function PostsPage() {
return (
<div>
<h1>All Posts</h1>
{/* Rest of the page */}
<AddPostForm />
</div>
);
}
8. Suspense and Streaming
Next.js leverages React Suspense for handling loading states and streaming content to improve user experience.
Using Loading Files
Create a loading.tsx
file at the same level as your page.tsx
to show loading states:
// posts/loading.tsx
export default function Loading() {
return (
<div className="flex justify-center items-center min-h-[200px]">
<div className="animate-spin h-10 w-10 border-4 border-blue-500 rounded-full border-t-transparent"></div>
</div>
);
}
Using Suspense Manually
For more granular control, use Suspense directly:
import { Suspense } from 'react';
async function PostsList() {
// Fetch data that might take time
const posts = await getPosts();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default function PostsPage() {
return (
<div>
<h1>All Posts</h1>
{/* Only the PostsList will show loading state, not the entire page */}
<Suspense fallback={<div>Loading posts...</div>}>
<PostsList />
</Suspense>
</div>
);
}
9. Caching, Static & Dynamic Rendering
Next.js has sophisticated caching mechanisms for both data and rendered content.
Data Fetching with Cache Control
async function getProducts() {
// Default: cache until manually invalidated
const response = await fetch('https://dummyjson.com/products?limit=5');
// No cache: refetch on every request
const response = await fetch('https://dummyjson.com/products?limit=5', {
cache: 'no-store'
});
// Revalidate cache after a specific time
const response = await fetch('https://dummyjson.com/products?limit=5', {
next: { revalidate: 3600 } // 1 hour
});
return response.json();
}
Controlling Static vs Dynamic Rendering
To force a route to be dynamic in production:
// In posts/page.tsx
export const dynamic = 'force-dynamic';
export default function PostsPage() {
// This will be rendered on each request
}
10. Middleware
Middleware allows you to run code before a request is completed, perfect for authentication, redirects, or headers.
// middleware.ts (in the root or src/ directory)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check if user is authenticated
const isAuthenticated = false; // Replace with actual auth check
// If not authenticated and trying to access protected routes
if (!isAuthenticated &&
(request.nextUrl.pathname.startsWith('/dashboard') ||
request.nextUrl.pathname.startsWith('/account'))) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Continue with the request
return NextResponse.next();
}
// Configure which paths should run the middleware
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};
11. Folder Structure
A well-organized Next.js project typically follows this structure:
project-root/
├── src/
│ ├── app/ # App Router pages
│ │ ├── page.tsx # Home page
│ │ ├── layout.tsx # Root layout
│ │ ├── favicon.ico # Favicon
│ │ └── posts/ # Posts route
│ │ ├── page.tsx # Posts page
│ │ ├── loading.tsx # Loading UI
│ │ └── [id]/ # Dynamic route
│ │ └── page.tsx # Individual post page
│ ├── components/ # Shared components
│ │ ├── ui/ # UI components
│ │ └── feature/ # Feature-specific components
│ ├── lib/ # Utility functions, shared libs
│ └── styles/ # Global styles
├── public/ # Static assets
├── prisma/ # Prisma schema and migrations
│ └── schema.prisma
├── .env # Environment variables (committed)
├── .env.local # Local environment variables (not committed)
├── next.config.js # Next.js configuration
└── package.json # Project dependencies
12. Static Export and Deployment
Static Export
For static site generation without a Node.js server:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
// When using static export with Image component:
images: {
unoptimized: true,
}
};
module.exports = nextConfig;
Then run:
npm run build
This creates a static site in the out
directory that can be deployed to any static hosting service.
Standard Deployment
For a full Next.js application with server components and server actions:
- Push your code to GitHub:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/yourusername/your-repo.git
git push -u origin main
- Deploy to a platform like Vercel, Netlify, or set up a custom server with:
npm run build
npm run start
Environment Variables
Create these files for environment variables:
.env
: Base variables committed to git (non-sensitive).env.local
: Local variables not committed (secrets)
Example .env
file:
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=file:./dev.db
Conclusion
Next.js provides a powerful framework for building modern web applications, combining the best of server-side rendering with client-side interactivity. The 12 concepts covered in this guide-from project setup to deployment-give you the foundation to create high-performance, SEO-friendly, and user-friendly applications.
Remember that Next.js is evolving rapidly, with new features and improvements being added regularly. Stay updated with the official documentation for the latest best practices and features.