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.

Nish Sitapara
Next.jsTutorialWeb DevelopmentFull-Stack

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:

  1. 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
  1. 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.

Comments

Join the discussion on “NextJS Tutorial - 12 Essential Concepts You Need to Know

3 Comments

J

John Doe

Nov 15, 2023

Great article! I learned a lot from this.

J
Jane Smith
Nov 15, 2023

I agree! The technical details were very clear.

A

Anonymous

Nov 16, 2023

I have a question about the third point you made. Could you elaborate more on that?

J

Jane Smith

Nov 17, 2023

This is exactly what I was looking for. Thanks for sharing your insights!

A
Anonymous
Nov 17, 2023

Could you share what specifically you found helpful?

J
Jane Smith
Nov 17, 2023

The implementation details in the middle section were exactly what I needed for my project.