Next.js Foundations
Learn how to build a full-stack web application with the free, Next.js Foundations course.
Getting Started
Create a new Next.js application using the dashboard starter example and explore the project.
Welcome! This course will guide you through building a full-stack Next.js application. We'll start from scratch and cover all the essential features. To begin, open your terminal and run the following command to create a new Next.js app using the official dashboard starter template.
npx create-next-app@latest nextjs-dashboard --use-npm --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example"Code Breakdown:
npx create-next-app@latest: This is the command that runs the Next.js app creation tool.npxallows you to run package executables without installing them globally.nextjs-dashboard: This is the name of the directory that will be created for your new project.--use-npm: This flag tells the tool to usenpmas the package manager instead of the default `yarn`.--example "...": This flag specifies a template to clone from. In this case, we are using an official Vercel learning template.
Once the installation is complete, navigate into your new project directory and start the development server:
cd nextjs-dashboard
npm run devCode Breakdown:
cd nextjs-dashboard: Changes the current directory in your terminal to your new project folder.npm run dev: This command starts the Next.js development server, which includes features like live reloading and error reporting.
Open http://localhost:3000 in your browser. You should see the starter homepage. The project structure includes key folders like app for routing, public for static assets, and ui for our user interface components.
CSS Styling
Style your Next.js application with Tailwind and CSS modules.
Next.js offers multiple ways to style your application. This project uses Tailwind CSS, a utility-first CSS framework for rapid UI development.
You can style elements by adding Tailwind classes directly in your JSX. For example, to create a blue button:
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Click me
</button>Code Breakdown:
className="...": In React, you useclassNameinstead ofclassto apply CSS classes.bg-blue-500: A Tailwind utility class that sets the background color.hover:bg-blue-700: A pseudo-class that changes the background color on hover.text-white, font-bold, py-2, px-4, rounded: These are other utility classes for text color, font weight, padding, and border radius.
For component-specific styles that don't leak into the global scope, you can use CSS Modules. Create a file named MyComponent.module.css and import it into your component:
/* In MyComponent.module.css */
.error {
color: red;
}
// In your component
import styles from './MyComponent.module.css';
function MyComponent() {
return <p className={styles.error}>This is an error.</p>;
}Code Breakdown:
*.module.css: The.module.cssextension tells Next.js to treat this as a CSS Module.import styles from ...: When imported, the CSS Module exports an object (styles) where keys correspond to your class names.className={styles.error}: Next.js automatically generates a unique class name (e.g.,MyComponent_error__12345) to prevent style conflicts with other components.
Optimizing Fonts and Images
Optimize fonts and images with the Next.js built-in components.
Next.js provides built-in components for optimizing fonts and images, which are crucial for good performance and Core Web Vitals.
Fonts
Use `next/font` to automatically host font files, preventing layout shifts and ensuring privacy.
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function Layout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}Code Breakdown:
import { Inter } from 'next/font/google': Imports the desired font function from Google Fonts.const inter = Inter(...): Initializes the font, specifying subsets to optimize for performance.className={inter.className}: Applies the font's class name to the<html>element, ensuring it's used throughout the app. Next.js handles loading the font CSS automatically.
Images
The <Image> component from next/image optimizes images by resizing, lazy-loading, and serving them in modern formats like WebP.
import Image from 'next/image';
function Profile() {
return (
<Image
src="/profile.png"
alt="My Profile Picture"
width={500}
height={500}
/>
);
}Code Breakdown:
src="/profile.png": The path to your image file, located in thepublicdirectory.alt="...": Alternative text for accessibility, describing the image content.width={500} height={500}: These props are required to prevent layout shift. Next.js uses them to reserve space for the image while it loads.
Creating Layouts and Pages
Create the dashboard routes and a shared layout that can be shared between multiple pages.
In the App Router, a page is UI that is unique to a route. You can create a page by exporting a component from a page.tsx file.
A layout is UI that is shared between multiple pages. Create a layout.tsx file to define a shared layout. It must accept a children prop that will be populated with a child layout or page.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: { children: React.ReactNode }) {
return (
<section>
{/* Include shared UI here, e.g., a sidebar */}
<nav>Dashboard Sidebar</nav>
{children}
</section>
);
}Code Breakdown:
app/dashboard/layout.tsx: The file path defines the route segment (/dashboard) that this layout applies to.{ children }: React.ReactNode: Thechildrenprop is a required prop that will be filled by Next.js with the content of the page or a nested layout.<section>...</section>: This is the shared UI. The{children}prop is placed where the page-specific content should be rendered.
Any pages inside the app/dashboard directory, like app/dashboard/settings/page.tsx, will automatically be wrapped with this layout.
Setting Up Your Database
Setup a database for your application and seed it with initial data.
For this course, we'll use Vercel Postgres, but you can use any PostgreSQL provider. After creating a database, you'll get a connection string.
Add the connection string to a .env file in the root of your project:
POSTGRES_URL="postgres://..."Code Breakdown:
.env: This file stores environment variables, which are secret keys and configuration values that should not be committed to version control.POSTGRES_URL: The name of the variable that will hold your database connection string.
Next, install the Vercel Postgres SDK: npm install @vercel/postgres.
To seed your database with initial data, you can create a script. For example, scripts/seed.js:
// scripts/seed.js
const { db } = require('@vercel/postgres');
async function main() {
const client = await db.connect();
// ... (CREATE TABLE and INSERT statements)
await client.end();
}
main().catch((err) => console.error(err));Code Breakdown:
const { db } = require('@vercel/postgres'): Imports the database client from the SDK.await db.connect(): Establishes a connection to the database.// ... (CREATE TABLE and INSERT statements): This is where you would write your SQL commands to define your tables and populate them with initial data.await client.end(): Closes the database connection.
Run the script from your package.json: "seed": "node -r dotenv/config ./scripts/seed.js".
Fetching Data
Learn about the different ways to fetch data in Next.js, and fetch data for your dashboard page using Server Components.
In the App Router, React Server Components are the default. This allows you to fetch data directly within your components on the server.
You can fetch data at the component level, which is great for co-locating data fetching logic with the UI that uses it.
// app/dashboard/page.tsx
import { fetchCardData } from '@/app/lib/data';
export default async function Page() {
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices
} = await fetchCardData();
return (
<main>
{/* ... use the fetched data ... */}
</main>
);
}Code Breakdown:
async function Page(): By making the Page componentasync, you can useawaitinside it to fetch data before rendering.await fetchCardData(): This calls your data-fetching function. The component will wait for this promise to resolve before it renders and sends the final HTML to the client.- Server Component: Because there is no
'use client'directive, this component renders entirely on the server. The fetched data is part of the initial HTML payload.
Next.js automatically deduplicates fetch requests. If you use a database client, you should manually cache your data fetches using React.cache to avoid re-fetching the same data in a single render pass.
Static and Dynamic Rendering
Understand how rendering works in Next.js, and make your dashboard app dynamic.
By default, Next.js will use Static Rendering. Routes are rendered at build time, making them fast, always available, and cachable.
You can opt into Dynamic Rendering when you have data that changes frequently. This renders the route for each user at request time.
Dynamic rendering is triggered by using dynamic functions like cookies(), headers(), or by using the unstable_noStore API.
import { unstable_noStore as noStore } from 'next/cache';
export async function fetchInvoices() {
noStore(); // This opts the component into dynamic rendering
// ... data fetching logic ...
}Code Breakdown:
import { unstable_noStore }: Imports the specific API from Next.js to control caching behavior.noStore(): Calling this function inside a data-fetching function tells Next.js that this route should not be statically cached. It must be re-rendered for every incoming request to ensure the data is always fresh.
Streaming
Improve your application's loading experience with streaming and loading skeletons.
Streaming allows you to break down the page into smaller chunks and progressively send them from the server to the client.
You can use React's <Suspense> boundary to stream a component. You provide a fallback UI (like a skeleton) to show while the component is loading.
import { Suspense } from 'react';
import { RevenueChart } from '@/app/ui/dashboard/revenue-chart';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
export default function Page() {
return (
//...
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
//...
)
}Code Breakdown:
<Suspense>: A built-in React component that lets you display a fallback while its children are loading.fallback={<RevenueChartSkeleton />}: Thefallbackprop takes a React component to render during the loading state.<RevenueChart />: This is the actual component that fetches its own data. While it's fetching, theRevenueChartSkeletonwill be displayed. Once ready, Next.js will stream the rendered HTML forRevenueChartto the client to replace the skeleton.
This improves user experience by showing content sooner, even if some parts of the page take longer to fetch or render.
Adding Search and Pagination
Add search and pagination to your dashboard application using Next.js APIs.
To implement search and pagination, you'll use URL search parameters to manage the state.
First, capture the user's input in a client component. Use the useRouter and usePathname hooks to update the URL with the search query.
'use client';
import { useSearchParams, usePathname, useRouter } from 'next/navigation';
export default function Search({ placeholder }: { placeholder: string }) {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}
}Code Breakdown:
'use client': Required as we're using client-side hooks.useRouter(), usePathname(), useSearchParams(): Hooks fromnext/navigationto interact with the URL.URLSearchParams: A standard browser API to easily manipulate URL query parameters.params.set('query', term): Adds or updates thequeryparameter in the URL.replace(...): Updates the URL in the browser without reloading the page. This triggers a new server render with the updated search parameters.
On the server, in your page component, read the search params from the searchParams prop and pass them to your data fetching function.
export default async function Page({
searchParams
}: {
searchParams?: { query?: string; page?: string; }
}) {
const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;
const invoices = await fetchFilteredInvoices(query, currentPage);
// ...
}Code Breakdown:
{ searchParams }: Next.js automatically passes the URL's search parameters as a prop to server-side Page components.const query = searchParams?.query || '': Safely access thequeryparameter.await fetchFilteredInvoices(query, currentPage): Your data fetching logic now uses the parameters from the URL to get the correct, filtered data from the database.
Mutating Data
Mutate data using React Server Actions, and revalidate the Next.js cache.
React Server Actions allow you to run asynchronous code directly on the server, triggered from client-side events. They eliminate the need to create separate API endpoints for data mutations.
Define a Server Action by adding the 'use server'; directive at the top of a function.
// In a file like 'app/lib/actions.ts'
'use server';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
export async function createInvoice(formData: FormData) {
// ... (validate and process formData)
await sql`INSERT INTO invoices ...`;
revalidatePath('/dashboard/invoices'); // Revalidate the cache and show new data
}Code Breakdown:
'use server': This directive marks the function as a Server Action, which can be called from client components but executes on the server.formData: FormData: The function automatically receives the form's data as aFormDataobject.await sql\`...\`: Your database mutation logic runs securely on the server.revalidatePath('/dashboard/invoices'): After the mutation, this tells Next.js to clear the cache for the specified path, forcing a re-fetch of the latest data on the next visit.
You can then call this action directly from a form's action attribute.
Handling Errors
Handle errors gracefully with error.tsx and notFound.
The error.tsx file convention allows you to gracefully handle unexpected runtime errors in nested routes. It automatically wraps a page or child layout in a React Error Boundary.
'use client';
export default function Error({ error, reset }: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<main>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</main>
);
}Code Breakdown:
'use client': Error boundaries must be Client Components.error: Error: A prop that contains the caught error instance.reset: () => void: A function prop that, when called, attempts to re-render the segment where the error occurred.
The notFound() function can be used to handle "not found" states. If called inside a route segment, it will render the closest not-found.tsx file.
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) {
const invoice = await fetchInvoiceById(params.id);
if (!invoice) {
notFound();
}
// ...
}Code Breakdown:
if (!invoice): Checks if the data for a dynamic route (e.g., a specific blog post) exists.notFound(): If the data is not found, calling this function stops further rendering and displays the nearestnot-found.tsxUI, returning a 404 status code.
Improving Accessibility
Implement server-side form validation and improve accessibility in your forms.
Web accessibility (a11y) is crucial for creating inclusive applications. Key practices include using semantic HTML, ensuring proper color contrast, and managing focus.
For forms, always associate labels with inputs using the htmlFor attribute. This helps screen readers announce what the input is for.
<label htmlFor="amount">Choose an amount</label>
<input id="amount" name="amount" type="number" />Code Breakdown:
<label htmlFor="amount">: ThehtmlForattribute points to theidof the input it describes.<input id="amount" ... />: Theidmust match the label'shtmlForvalue to create the association.
When creating custom components, use ARIA (Accessible Rich Internet Applications) attributes to provide additional context to assistive technologies. For example, aria-live="polite" can be used to announce status updates.
Adding Authentication
Add authentication to protect your dashboard routes using NextAuth.js, Server Actions, and Proxy.
NextAuth.js is a complete open-source authentication solution for Next.js applications. To add it, install next-auth.
Configure your authentication providers (e.g., Google, GitHub, Credentials) in an API route at app/api/auth/[...nextauth]/route.ts.
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
export const { auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
// ... configure credentials provider
}),
],
});Code Breakdown:
[...nextauth]/route.ts: This is a "catch-all" API route that handles all authentication-related requests (e.g.,/api/auth/signin,/api/auth/callback).NextAuth({...}): The main function where you configure your authentication strategies.providers: [...]: An array where you define how users can sign in (e.g., with email/password viaCredentials, or with Google/GitHub).export const { auth, signIn, signOut }: These are helper functions and middleware handlers that you'll use throughout your application to manage sessions and protect routes.
You can protect routes by using middleware. Create a middleware.ts file in the root of your project to intercept requests and redirect unauthenticated users.
Adding Metadata
Learn how to add metadata to your Next.js application.
Next.js has a Metadata API that allows you to define metadata (e.g., title, description) for each page, which is important for SEO.
You can export a static metadata object from a layout.tsx or page.tsx file.
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Invoices | Acme Dashboard',
};Code Breakdown:
export const metadata: By exporting an object with this specific name, Next.js will automatically use it to generate the<head>tags for the page.title: '...': This will set the<title>tag for the browser tab and search results.
For dynamic routes, you can use the generateMetadata function to create metadata based on the current route parameters.
export async function generateMetadata({ params }): Promise<Metadata> {
const invoice = await fetchInvoiceById(params.id);
return { title: `Invoice #${invoice.id}` };
}Code Breakdown:
export async function generateMetadata(): By exporting anasyncfunction with this name, you can fetch data to dynamically create metadata.{ params }: The function receives the route's dynamic parameters (e.g., theidfrom/invoices/[id]).return { title: ... }: The returned object defines the metadata for that specific page, like the title for a unique invoice.
Next Steps
Next.js Dashboard Course Conclusion.
Congratulations on completing the Next.js Foundations course! You've learned the core concepts of building a full-stack application with the App Router.
Where to go from here?
- Explore the Next.js Documentation: Dive deeper into advanced features like Parallel Routes, Intercepting Routes, and more.
- Build Your Own Project: The best way to learn is by doing. Start a personal project to apply and solidify your knowledge.
- Join the Community: Engage with other Next.js developers on GitHub, Discord, and Reddit.
