Skip to content

richardrod93/LearnNextJS

Repository files navigation

Next.js App Router Course - Starter

This is the starter template for the Next.js App Router Course. It contains the starting code for the dashboard application.

For more information, see the course curriculum on the Next.js Website.

chapter 1 getting started:

/app: Contains all the routes, components, and logic for your application, this is where you'll be mostly working from.

/app/lib: Contains functions used in your application, such as reusable utility functions and data fetching functions.

/app/ui: Contains all the UI components for your application, such as cards, tables, and forms. To save time, we've pre-styled these components for you.

/public: Contains all the static assets for your application, such as images. Config Files: You'll also notice config files such as next.config.ts at the root of your application. Most of these files are created and pre-configured when you start a new project using create-next-app. You will not need to modify them in this course.

TypeScript You may also notice most files have a .ts or .tsx suffix. This is because the project is written in TypeScript. We wanted to create a course that reflects the modern web landscape.

For now, take a look at the /app/lib/definitions.ts file. Here, we manually define the types that will be returned from the database. For example, the invoices table has the following types:

export type Invoice = { id: string; customer_id: string; amount: number; date: string; // In TypeScript, this is called a string union type. // It means that the "status" property can only be one of the two strings: 'pending' or 'paid'. status: 'pending' | 'paid'; };

By using TypeScript, you can ensure you don't accidentally pass the wrong data format to your components or database, like passing a string instead of a number to invoice amount.

So on deployment of the webapp, typescript doesn't ever actuallly run. Its purely for dev help throwing red underline errors signalling where you're trying to create or recieve a object already declared in definitions.ts file so it throws erorr 100 for tring to pass the wrong data format to a component. also there are other errors it can catch too.

In dev mode, TypeScript:

Analyzes your code (using tsc or the Next.js compiler). Finds places where your code doesn’t match the declared types (like your definitions.ts).

Throws compile-time errors (the red underlines and error messages).

Example: TS100 — Type 'string' is not assignable to type 'number'

When you run:

next build ✅ Next.js compiles your .ts and .tsx files to plain JavaScript (.js) for server upload/webapp deployment.


chapter 2 CSS Styling:

Topics Covered:

  1. How to add a global CSS file to your application.

  2. Two different ways of styling: Tailwind and CSS modules.

  3. How to conditionally add class names with the clsx utility package.

GLOBAL CSS FILE- inside the /app/ui folder, you'll see a file called global.css. You can use this file to add CSS rules to all the routes in your application - such as CSS reset rules, site-wide styles for HTML elements like links, and more.

css reset is where, different browsers (Chrome, Safari, Firefox, etc.) have slightly different default CSS styles.CSS reset rules help you standardize styling across all browsers by removing inconsistent default styles. like when Some browsers add extra margin to or

by default. example:

input[type='number']::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }

You can import global.css in any component in your application, but it's usually good practice to add it to your top-level component. In Next.js, this is the root layout (more on this later).

to Add global styles to your application go to /app/layout.tsx and importing the global.css file(put this at top): import '@/app/ui/global.css';

then in the global.css you have: @tailwind base; → sets up Tailwind’s CSS reset (like Normalize/Preflight).

@tailwind components; → makes custom Tailwind components (like buttons or cards) available.

@tailwind utilities; → loads all the utility classes (like flex, min-h-screen, p-6, etc.).

final summary: You import the '@/app/ui/global.css'; into my layout.tsx so that when the page() is rendered at the root DOM. the layout file is at the same time recognizing

as function calls to the @tailwind base; @tailwind components; @tailwind utilities; css libraries as it renders the page() But Tailwind itself doesn’t “call functions” at runtime — it’s already compiled to CSS during build time (via PostCSS and your tailwind.config.js).

just like how Next.js compiles your .ts and .tsx files to plain JavaScript (.js) for server upload/webapp deployment.

benifits to this styling style: Although the CSS styles are shared globally, each class is singularly applied to each element. This means if you add or delete an element, you don't have to worry about maintaining separate stylesheets, style collisions, or the size of your CSS bundle growing as your application scales.

OTHER STYLING METHOD CSS modules:

CSS Modules allow you to scope CSS to a component by automatically creating unique class names, so you don't have to worry about style collisions

so css modules is like custome styles you make and tailwinds(global.css) is using pre made styles

here is the same black triangle from the last style method:

.shape { height: 0; width: 0; border-bottom: 30px solid black; border-left: 20px solid transparent; border-right: 20px solid transparent; }

put that in file called home.module.css inside /app/ui

Then, inside your /app/page.tsx file import the styles:

import styles from '@/app/ui/home.module.css';

sidenote: Why you don’t need the full path The @ alias (or sometimes ~ or other aliases) is configured in your project to represent the root of your project directory (like nextjs-dashboard/). this is done by adding "baseUrl": ".", "paths": { "@/": ["./"] } to the tsconfig.json file

finally replace the Tailwind class names from the

you changed beforereplace it with styles.shape:

Using the clsx library to toggle class names:

There may be cases where you may need to conditionally style an element based on state or some other condition. example:

InvoiceStatus component which accepts status. The status can be 'pending' or 'paid'. If it's 'paid', you want the color to be green. If it's 'pending', you want the color to be gray.

  1. create a new tsx file in a folder w/ a label that corrilates to the purpose of the clsx driven component you're about to store in it and heres one example

import clsx from 'clsx';

export default function InvoiceStatus({ status }: { status: string }) { return ( <span className={clsx( 'inline-flex items-center rounded-full px-2 py-1 text-sm', { 'bg-gray-100 text-gray-500': status === 'pending', 'bg-green-500 text-white': status === 'paid', }, )} > // ... )}

this UI component gets rendered if the user is on the invoice page. Visable in green or grey dependng on statues

A final way to style:

Sass which allows you to import .css and .scss files. or CSS-in-JS libraries such as styled-jsx, styled-components, and emotion.


CHAPTER 3 Optimizing Fonts and Images:

topics covered:

  1. How to add custom fonts with next/font.

  2. How to add images with next/image.

  3. How fonts and images are optimized in Next.js.

  4. How fonts and images are optimized in Next.js:

using custom fonts in your project can affect performance if the font files need to be fetched and loaded

With fonts, layout shift happens when the browser initially renders text in a fallback or system font and then swaps it out for a custom font once it has loaded. This swap can cause the text size, spacing, or layout to change, shifting elements around it.

Next.js automatically optimizes fonts in the application when you use the next/font module. it downloads font files at build time and hosts them with your other static assets

  1. How to add custom fonts with next/font:

Adding a primary font- add a custom Google font to your application to see how this works

first In /app/ui create a new file called fonts.ts this file to keep the fonts that will be used throughout your application.

Next add this in the file. it will import the font library from google and export the one we want to use to we can then import into layout.tsx

import { Inter } from 'next/font/google';

export const inter = Inter({ subsets: ['latin'] });

Next add the font to the element in /app/layout.tsx:

at the top: import { inter } from '@/app/ui/fonts';

in the return element add a class name attribute set to out inter font created from the google font lib.

side note: the classname attribute is one way to import different css styling into a element

side note: This is a template literal (backticks: ``) in JavaScript.we used it to combine props and text in the react book. Then ${} syntax lets you inject dynamic values (like variables) inside the string.

Here, you’re saying: “Take whatever inter.className is, and combine it with the literal antialiased.”

Navigate to your browser, open dev tools and select the body element. You should see Inter and Inter_Fallback are now applied under styles.

how to add a secondary font:

You can also add fonts to specific elements of your application.

in font.ts import font called Lusitana and pass it to the

element in your /app/page.tsx file.

side note: In addition to specifying a subset like you did before, you should also specify different font weights. For example, 400 (normal) and 700 (bold).

add Lusitana in fonts.ts like this: import { Inter, Lusitana } from 'next/font/google';

export const lusitana = Lusitana({ weight: ['400', '700'], subsets: ['latin'], });

then import it to pages.tsx at the top put: import { lusitana } from '@/app/ui/fonts';

Then use it in page.tsx wrapping the paragraph "Welcome to Acme. This is the example for the Next.js Learn Course, brought to you by Vercel." and remove the old

styling

side note: using Inter in the layout page so that is gets globally rendered Because layout.tsx is the root layout in Next.js, every page that renders inside this layout will inherit the Inter font by default. and Use Inter everywhere, but for this one

element, I want Lusitana instead! also is a lago in the lusitana font library.

  1. How to add images with next/image:

Next.js can serve static assets, like images, under the top-level /public folder. Files inside /public can be referenced in your application.

HTML way to add image: Screenshots of the dashboard project showing desktop version

Issue with this method is it doesn't:

  1. Ensure your image is responsive on different screen sizes.
  2. Specify image sizes for different devices.
  3. Prevent layout shift as the images load.
  4. Lazy load images that are outside the user's viewport.

the above actions are some examples of image optimization.

Solution: you can use the next/image component to automatically optimize your images.

The Component is an extension of the HTML tag, and comes with automatic image optimization

inside the /public folder there are two images: hero-desktop.png and hero-mobile.png. These two images are completely different, and they'll be shown depending if the user's device is a desktop or mobile.

In your /app/page.tsx file, import the component from next library. Then, add the image in the code

add this to page.tsx:

at top import component import Image from 'next/image';

then add the Image component to render and optimize it:

Screenshots of the dashboard project showing desktop version

side note:

className="hidden md:block"

is a tailwind css class that make the image rendered Invisible on small screens (like mobile) and Visible on medium screens (like desktops).

and no not like the programming structure class where you create objects of data.

styling class is just a set of rules for visually displaying something and in this case lusitana.className is the set of rules in the lusitana font for how the text will be visually displayed

side note for images:

Layout shift happens when the browser loads something without knowing its size, so it doesn’t leave enough space at first.

When the image or font finally loads, it “pushes” other content around, causing a visible jump — that’s a layout shift.

When you provide width and height in the component Next.js (and the browser) reserves that exact space before the image even loads. Result: No layout shift.

CHAPTER 4 Creating Layouts and Pages:

So far, your application only has a home page. Let's learn how you can create more routes with layouts and pages.

topics covered:

  1. Create the dashboard routes using file-system routing.

  2. Understand the role of folders and files when creating new route segments.

  3. Create a nested layout that can be shared between multiple dashboard pages.

  4. Understand what colocation, partial rendering, and the root layout are.

Next.js uses file-system routing where folders are used to create nested routes. Each folder represents a route segment that maps to a URL segment.

You can create separate UIs for each route using layout.tsx and page.tsx files.

page.tsx is a Next.js file that exports a React component, and it's required for the route to be accessible.

/app/page.tsx - this is the home page associated with the route /.

To create a nested route, you can nest folders inside each other and add page.tsx files inside them EXAMPLE: Creating the dashboard page

/app/dashboard/page.tsx will be the path of the new dashboard page/route

  1. Create a new folder called dashboard inside /app

  2. create a new page.tsx file inside the dashboard

  3. fill it with a defualt export function for exporting and rendering in the nearest layout.tsx:

export default function Page() { return

Dashboard Page

; }

Now you should be able to render this spesific page by going to its url route http://localhost:3000/dashboard

summarry: to create a new route segment use a folder, and add a page file inside it.

By having a special name page files, Next.js allows you to colocate UI components, test files, and other related code within your routes.

what is co-locating: /app /dashboard page.js ← page route for /dashboard DashboardCard.js ← UI component just for dashboard dashboardtest.js ← test file for dashboard

Next.js doesn’t care that these files are next to page.js in the file/route It will only treat page.js as the route, and it ignores DashboardCard.js and dashboard.test.js as page routes.

special name refers to the literal naming convention settup that nextjs recognizes when a file named page is in a route/folder when the route is getting rendered in the browser it looks for the page/special name in that route

then nextjs finds the closest layout.js file above or beside the page.js file in the folder hierarchy and wraps the page with that layout.

If you want multiple pages under the same section, create subfolders:

/app /dashboard page.js → /dashboard /stats page.js → /dashboard/stats /settings page.js → /dashboard/settings

practise, create two new sections/pages inside the dashboard route /app/dashboard/invoices/page.tsx and a /app/dashboard/customers/page.tsx like the above example

Creating the dashboard layout:

side note: Global layout → shared by all. Nested layout → optional, scoped to specific sections. Pages → get wrapped in both global and any nested styling depending on placement. The global layout.tsx is located in the main app folder. You can then add another layout inside the subfolder app/dashboard/ and the pages in there will get wrapped by both becuase layout works as a hierarchical system. Global layout: effects styling by all pages. Dashboard layout: wraps only the dashboard section (and inherits global layout’s styles too).

Inside the /dashboard folder, add a new file called layout.tsx and paste the following code:

import SideNav from '@/app/ui/dashboard/sidenav';

export default function Layout({ children }: { children: React.ReactNode }) { return (

{children}
); }

In this layout you're importing the component into your layout. Any components/pages that are under this spesific route will get rendered with the local styles or the layout in this route aswell as the global styles imported in the global layout in /app

sidenote: One benefit of using layouts in Next.js is that on navigation, only the page components update while the layout won't re-render. This is called partial rendering which preserves client-side React state in the layout when transitioning between pages. which means the SideNav bar never has to rerender next.js is able to only update what is needed. which also means No flicker or re-mounting of the sidebar when you transition between pages in this route


Chapter 5 Navigating Between Pages:

In the previous chapter, you created the dashboard layout and pages. and had to add a /routename into the url to get to that page. Now, we will add some links to allow users to navigate between the dashboard routes in he UI

topics covered:

  1. How to use the next/link component.

  2. How to show an active link with the usePathname() hook

  3. How navigation works in Next.js

Optimizing navigation:

In normal HTML (and in React if you don’t use a special router like Next.js’s Link), you’d create a UI link to another page like this: Home

The downside to using is when you navigate between routes such as home invoices, customers, ect. the whole page will need to reload.

To avoid a full page reload upon user click lets replace element with component from the nextjs library:

The current layout.tsx code uses a custom component created and imported from '@/app/ui/dashboard/sidenav';

In SideNav you'll find it uses , another custom component imported from @\app\ui\dashboard\nav-links.tsx

In component the method for rendering the navigation bar page links uses lets replace that with

side note before we change the code:

allows you to do client-side navigation with JavaScript. while is a server-side link navigation element. and even though in the source code for you'll find under the hood, using the over is still better becuase 1. Smooth, client-side routing 2. Preserved layouts and state 3. No flickers or reloads 4. Preloading on hover

code from opensource JS library: return ( <a {...restProps} {...childProps}> {children} )

Now lets intagrate the component for navigation bar links, open /app/ui/dashboard/nav-links.tsx

last side note before change: The code already use for other stuff but we're not focusing on those DOMS. we're using the navigation bar links in the dashboard as an example.

Make these changes:

  1. add to top: import Link from 'next/link';

  2. Then, replace the tag with :

old: return(

{link.name}

);

new:

    return (
      <Link
        key={link.name}
        href={link.href}
        className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
      >
        <LinkIcon className="w-6" />
        <p className="hidden md:block">{link.name}</p>
      </Link>
    );

CODE FLOW EXPLANATION FROM NAV-LINKS.tsx to LAYOUT.tsx:

FILE 1: NavLinks.tsx This file defines and renders the navigation links.

first it Imports icons and component import { HomeIcon, DocumentDuplicateIcon, UserGroupIcon } from '@heroicons/react/24/outline'; import Link from 'next/link';

second it Defines an array of links (this data usualy stored in a database) const links = [ { name: 'Home', href: '/dashboard', icon: HomeIcon }, { name: 'Invoices', href: '/dashboard/invoices', icon: DocumentDuplicateIcon }, { name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon }, ];

finally this part tells the code to loop through all the links in links: links.map((link) => { /* build each dom from each object in the array during each loop */ })

links is the array you’re looping through.

.map() is a callback function.

(link) is the name we give each item/object in that array, you give it a temporary variable name inside the loop — here, it’s named link. That name link can be anything — you could write links.map((banana) => banana.name) and it would still work (but be confusing!).

SIDE NOTE: here are some other method that can be performed on a array of objects: Goal Method Loop through items .map() Add new item to end .push() Remove item .filter() Modify items .map() with updated logic

finally this file/component is exported as NavLinks()

FILE 2: sidenav.tsx

First this file imports NavLinks() fromt the last file

this component then incorperated into layout/design style for this SideNav() component

Finally it exports the complete UI chunk (SideNav) to be used across pages.

FILE 3: Layout.tsx

first the SideNav() UI/component is imported

Finally the component is placed it the layout.tsx file for page rendering

summary for chapter 4 and 5(without usePathname()):

🔀 Code Splitting by Routes (Thanks to page.tsx and layout.tsx)

Next.js automatically splits your app's code per route, so each page (e.g., /dashboard, /login) has its own separate bundle.

This is different from traditional React SPAs (Single Page Apps), where all the code for all routes might get bundled and downloaded upfront.

Benefit: faster load times, isolated errors, and more efficient updates.

⚡ Using for Optimized Client-Side Navigation

When you use the component from next/link, it does more than just create a link:

It enables client-side routing (no full-page refresh).

In production, when a is visible in the viewport, Next.js prefetches the JS and data for that destination route in the background.

So when the user clicks the link, the page loads instantly — the code is already there.

      Pattern: Showing active links

A common UI pattern is to show an active link to indicate to the user what page they are currently on.

HOW TO: 1. you need to get the user's current path from the URL. To do this Nextjs provides a hook called usePathname() that you can use to check the path and save it to a variable for use.

Since usePathname() is a React hook, you'll need to turn nav-links.tsx into a Client Component. To do this Add React's 'use client' directive to the top of the file, then import usePathname() from next/navigation: EXAMPLE CODE:

'use client'; <--- add to very top of code

Next, import usePathname() from the next/navigation library and assign the function to a variable called pathname inside your for use later

Example:

import { usePathname } from 'next/navigation'; <--this at top

const pathname = usePathname(); <--declare this in NavLinks() code block

Last, lets use the current "pathname"

In the className(React prop that applies CSS classes locally) for in NavLinks() we will use clsx() to conditionally combine CSS class names It helps simplify class logic and avoid messy string concatenation in JSX.

clsx() example: const isActive = true; const isDisabled = false; const isError = true; const className = clsx( 'base-class', { 'active-class': isActive, 'disabled-class': isDisabled, 'error-class': isError, } ); if isActive is true apply include the string 'active-class' <-above code explained

Side Note: clsx() can handle as many conditional statements as you need. You can pass:

Strings (always included) Objects ({ className: condition }) Arrays of the above

Example: replace your static styling className with the condtional clsx() function

OLD className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"

NEW className={clsx( 'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3', { 'bg-sky-100 text-blue-600': pathname === link.href, }, )}

above explained: always include--> 'stuff in this tics' apply this --> 'bg-sky-100 text-blue-600' if pathname === link.href is true stament

SideNote: When to make it a client component If you were to add:

  • react hooks like useState, useEffect, or any browser-only logic -Event handlers like onClick -LocalStorage, window, document, etc. -You can also use DOM APIs like window, document, etc. -Next.js client-only hooks like usePathname, useSearchParams, and useRouter also become usable.

     Keep it a Server Component when...
    
  • No browser-only APIs are needed You’re not using window, document, or other DOM-specific features.

  • No client-side React hooks You don't need useState, useEffect, usePathname, etc.

  • You’re just rendering data Example: fetching and displaying content like a blog post, invoice list, or user profile using fetch() or database queries (e.g., Prisma, SQL).

  • You want faster page loads Server Components are smaller (less JavaScript sent to the browser). They reduce the bundle size because they don’t get shipped to the client.

  • You need to protect logic or data Keeping data-fetching or logic on the server keeps it hidden from the user — ideal for security and sensitive operations.

Chapter 6 Setting Up Your Database:

Any database provider will work

But before you can continue working on your dashboard, you'll need some data. Lets set up PostgreSQL database

  1. Push your project to GitHub. if not already done

  2. Set up a Vercel account and link/import your GitHub repo for instant previews and deployments.

Vercel is like AWS, but much more specialized and simplified. Its a deployment pipeline: It connects to GitHub, GitLab, or Bitbucket, and auto-deploys on commits. Meanwhile it gives you a dashboard to manage your DB, domains, usage ect..

  1. Create and link your project to a Postgres database. This is a form you must fill out once the repo is linked then at the bottum on the form hit deploy. -select the Storage tab from your project dashboard -Select Create after choosing a Marketplace Database Provider

side note: Serverless means you don't manage or maintain the server. It still runs on the cloud, the provider automatically uploads your repo/app to a cloud server and you still need a separate database for storing data.

-Neon makes you choose region and weather your want AUTH ot NO AUTH before deploying

ON (Auth enabled) the app will have: -user sign-up and login -Store user credentials (safely) in a PostgreSQL database -Include logic for user sessions, auth tokens, etc. which can come with: -Built-in UI (login forms) -Middleware to protect routes -Integration with providers (GitHub, Google, etc.)

OFF (Auth disabled): -Have no user accounts or authentication -Be open to anyone (no sign-in required) -Not store user identity or Which is good for: -Public dashboards -Static marketing sites -Anonymous apps

choose pricing option and continue

next name it nextjs-dashboard-postgres and hit create

Then once connected, navigate to the .env.local tab, click Show secret and Copy Snippet sidenote: Make sure you reveal the secrets before copying them. If you copy a secret while it's hidden: You’ll just paste ******** or an empty string — not the actual secret value.

Navigate to your code editor and rename the .env.example file to .env then Paste in the copied contents from Vercel.

Important: Go to your .gitignore file and make sure .env is in the ignored files to prevent your database secrets from being exposed when you push to GitHub.

So bassically the only people who have the DB credentials is your local file and vercel for preivew capabilities but not your github where someone could see them.

go to the created DB and ht connect and allow acess for editing.

Now to visually see the Neon DB Tables go to the neon console and go to tables

  1. Seed the database with initial data.

since it is now on and connected

The books wants you to use a API or .ts file you can access in the browser by accessing the route localhost:3000/seed that runs a SQL script written in the seed folder called route.ts to create the tables, and the data from placeholder-data.ts file to populate them after they've been created.

after you should get this message in browser: {"message":"Database seeded successfully"}

refresh the neon console and look at the new tables you created and populated this is the DB IDE built into vercel

Executing queries:

execute a query to make sure everything is working

use Router Handler, app/query/route.ts, to query the database. Inside this file, you'll find a listInvoices() function that has the following SQL query. The script lines were commented out so uncomment and go to localhost:3000/query to access file.

CHAPTER 7 FETCHING DATA: different ways you can fetch data for your application, and build out your dashboard overview page.

Topics covered

  1. Learn about some approaches to fetching data: APIs, ORMs, SQL, etc.

  2. How Server Components can help you access back-end resources more securely.

  3. What network waterfalls are.

  4. How to implement parallel data fetching using a JavaScript Pattern.

APPROACHES TO FECTHING DATA

  1. API layer

APIs are an intermediary layer between your application code and database. Like the .ts script we used to fecth the customer info in chapter 6 that query.ts file is considered an API

basically if an API executes in the server its in the api layer Query.ts used export async function GET() — this is how Next.js App Router defines HTTP endpoints.

It runs on the server, not in the browser.

It interacts with a database (postgres) and returns JSON via Response.json().

It can be accessed by a frontend via a fetch request

  1. Database queries

In full-stack APPS if you have to write logic to interact with your database

Case uses for Database queries -When creating your API endpoints, you need to write logic to interact with your database. -If you are using React Server Components (fetching data on the server), you can skip the API layer, and query your database directly without risking exposing your database secrets to the client.

side note: you should not query your database directly when fetching data on the client as this would expose your database secrets.

side note: while server apis act as a bridge between the frontend and backend systems and get Called by e.g: GET() through the frontend, mobile apps, other services, etc.Can include auth, input validation, error handling fo scurity EXAMPLE: export async function GET() { const data = await listInvoices(); return Response.json(data); // sends JSON to frontend }

Database query directly reads/writes data from the database using the backend/server code it Runs inside that function or backend logic e.g: SELECT * FROM invoices fetches the raw data from the DB. it has no security by itself None by and needs to be secured in backend logic EXAMPLE: const data = await sqlSELECT invoices.amount, customers.name FROM invoices JOIN customers ON invoices.customer_id = customers.id;;

  1. Using Server Components to fetch data

By default, Next.js applications use React Server Components benefits of using them:

-Server Components support JavaScript Promises, providing a solution for asynchronous tasks like data fetching natively. You can use async/await syntax without needing useEffect, useState or other data fetching libraries.

-Server Components run on the server, so you can keep expensive data fetches and logic on the server, only sending the result to the client.

-Since Server Components run on the server, you can query the database directly without an additional API layer. This saves you from writing and maintaining additional code.

Using SQL (skipping this part)

Now go to /app/lib/data.ts. Here you'll see that we're using postgres. The sql function allows you to query your database: import postgres from 'postgres';

const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });

// ...

You can call sql anywhere on the server, like a Server Component. But to allow you to navigate the components more easily, we've kept all the data queries in the data.ts file, and you can import them into the components.

Fetching data for the dashboard overview page

Now that you understand different ways of fetching data, let's fetch data for the dashboard overview page.

Now navigate to /app/dashboard/page.tsx, paste the following code, and spend some time exploring it:

code to long to paste here check file

The page is an async server component. This allows you to use await to fetch data. The async keyword makes a function asynchronous, which means:

It automatically returns a Promise

You can use await inside it to pause and wait for other async actions

This code presents 3 components which recieve data

  1. To fetch data for the component, import the fetchRevenue function from data.ts and call it inside your component: EXAMPLE

import { fetchRevenue } from '@/app/lib/data';

export default async function Page() { const revenue = await fetchRevenue();

this will assist the component in getting the DB data

Then Navigate to the component file (/app/ui/dashboard/revenue-chart.tsx) and uncomment the code inside it. this will assist the component in styling and UI logic

Now check localhost:3000/dashboard and you should see a chart that uses revenue data.

  1. For the component, we need to get the latest 5 invoices, sorted by date.

Instead of sorting through the latest invoices in-memory, you can use an SQL query to fetch only the last 5 invoices. For example, this is the SQL query from your data.ts file:

// Fetch the last 5 invoices, sorted by date const data = await sql<LatestInvoiceRaw[]> SELECT invoices.amount, customers.name, customers.image_url, customers.email FROM invoices JOIN customers ON invoices.customer_id = customers.id ORDER BY invoices.date DESC LIMIT 5;

Now uncomment the add this code EXAMPLE:

import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';

export default async function Page() { const revenue = await fetchRevenue(); const latestInvoices = await fetchLatestInvoices(); // ... }

you should see the Latest Invoices component now rendered in dashboard

  1. ractice: Fetch data for the components

Now it's your turn to fetch data for the components. The cards will display the following data:

Total amount of invoices collected. Total amount of invoices pending. Total number of invoices. Total number of customers.

Again, you might be tempted to fetch all the invoices and customers, and use JavaScript to manipulate the data. For example, you could use Array.length to get the total number of invoices and customers: EXAMPLE CODE:

const totalInvoices = allInvoices.length; const totalCustomers = allCustomers.length;

But with SQL, you can fetch only the data you need. It's a little longer than using Array.length, but it means less data needs to be transferred during the request. This is the SQL alternative:

EXAMPLE CODE: const invoiceCountPromise = sqlSELECT COUNT(*) FROM invoices; const customerCountPromise = sqlSELECT COUNT(*) FROM customers;

EXAMPLE CODE: import { fetchRevenue, fetchLatestInvoices,fetchCardData} from '@/app/lib/data';

// the card info gets imported as an object variable of data i handled changed the variables in // card DOM to extract the data from the array instead of how the book wanted which was // to save each incoming variable individually // example: // const { // numberOfInvoices, // numberOfCustomers, // totalPaidInvoices, // totalPendingInvoices, // } = await fetchCardData(); const cardInfo = await fetchCardData();

    <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
    <Card title="Collected" value={cardInfo.totalPaidInvoices} type="collected" />
    <Card title="Pending" value={cardInfo.totalPendingInvoices} type="pending" />
    <Card title="Total Invoices" value={cardInfo.numberOfInvoices} type="invoices" />
    <Card title="Total Customers" value={cardInfo.numberOfCustomers} type="customers"/>
  </div>
  1. What are request waterfalls?

However... there are two things you need to be aware of:

  1. The data requests are unintentionally blocking each other, creating a request waterfall.

  2. By default, Next.js prerenders routes to improve performance, this is called Static Rendering. So if your data changes, it won't be reflected in your dashboard. this will be discuessed in the next chapter

A "waterfall" refers to a sequence of network requests that depend on the completion of previous requests.

In the case of data fetching, each request can only begin once the previous request has returned data. EXAMPLE: const revenue = await fetchRevenue(); const latestInvoices = await fetchLatestInvoices();

fetchLatestInvoices is waiting for fetchRevenue to finish

This pattern is not necessarily bad. There may be cases where you want waterfalls because you want a condition to be satisfied before you make the next request. For example, you might want to fetch a user's ID and profile information first. Once you have the ID, you might then proceed to fetch their list of friends.

  1. Parallel data fetching

A common way to avoid waterfalls is to initiate all data requests at the same time - in parallel.

In JavaScript, you can use the Promise.all() or Promise.allSettled() functions to initiate all promises at the same time.

Now in data.ts apply this change to the fetchCardData()

const data = await Promise.all([
  invoiceCountPromise,
  customerCountPromise,
  invoiceStatusPromise,
]);

By using this pattern, you can:

Start executing all data fetches at the same time, which is faster than waiting for each request to complete in a waterfall. Use a native JavaScript pattern that can be applied to any library or framework.

There is one disadvantage of relying only on this JavaScript pattern: what happens if one data request is slower than all the others? Let's find out more in the next chapter.

SIDE NOTE: This is the way that would cause waterfalling

const invoiceCount = await invoiceCountPromise; const customerCount = await customerCountPromise; const invoiceStatus = await invoiceStatusPromise;

const data = [invoiceCount, customerCount, invoiceStatus];

CHAPTER 8 Static and Dynamic Rendering

we briefly discussed two limitations of the current setup:

-The data requests are creating an unintentional waterfall.

-The dashboard is static, so any data updates will not be reflected on your application.

In this chapter we will learn

  1. What static rendering is and how it can improve your application's performance.

  2. What dynamic rendering is and when to use it.

  3. Simulate a slow data fetch to see what happens.

  4. Different approaches to make your dashboard dynamic.

  5. What is Static Rendering?

data fetching and rendering happens on the server at build time (when you deploy) or when revalidating data. Whenever a user visits your application, the cached result is served.

benefits of static rendering:

--Faster Websites - Prerendered content can be cached and globally distributed when deployed to platforms like Vercel. This ensures that users around the world can access your website's content more quickly and reliably.

  • Reduced Server Load - Because the content is cached, your server does not have to dynamically generate content for each user request. This can reduce compute costs.

-SEO - Prerendered content is easier for search engine crawlers to index, as the content is already available when the page loads. This can lead to improved search engine rankings.

Static rendering is useful for UI with no data or data that is shared across users, such as a static blog post or a product page

not a good fit for a dashboard that has personalized data which is regularly updated.

The opposite of static rendering is dynamic rendering.

  1. What is Dynamic Rendering?

content is rendered on the server for each user at request time (when the user visits the page).

benefits of dynamic rendering:

  • Real-Time Data - Dynamic rendering allows your application to display real-time or frequently updated data. This is ideal for applications where data changes often.

  • User-Specific Content - It's easier to serve personalized content, such as dashboards or user profiles, and update the data based on user interaction.

  • Request Time Information - Dynamic rendering allows you to access information that can only be known at request time, such as cookies or the URL search parameters.

  1. Simulating a Slow Data Fetch

one problem mentioned in the previous chapter. What happens if one data request is slower than all the others?

Let's simulate a slow data fetch. In app/lib/data.ts, uncomment the console.log and setTimeout inside fetchRevenue():

// Artificially delay a response for demo purposes.
// Don't do this in production :)

// console.log('Fetching revenue data...');
// await new Promise((resolve) => setTimeout(resolve, 3000));

// console.log('Data fetch completed after 3 seconds.');

after uncommenting you will notice your page takes longer to load after page refresh

With dynamic rendering, your application is only as fast as your slowest data fetch.

CHAPTER 9 Streaming

Let's look at how you can improve the user experience when there are slow data requests.

Here are the topics we’ll cover:

  1. What streaming is and when you might use it.

  2. How to implement streaming with loading.tsx and Suspense.

3 What loading skeletons are.

  1. What Next.js Route Groups are, and when you might use them.

  2. Where to place React Suspense boundaries in your application.

  3. What is streaming?

Streaming is a data transfer technique that allows you to break down a route into smaller "chunks" and progressively stream them from the server to the client as they become ready.

By streaming, you can prevent slow data requests from blocking your whole page

The user can interact with parts of the page without waiting for all the data to load before any UI can be shown to the user.

works well with React's component model, as each component can be considered a chunk

There are two ways you implement streaming in Next.js:

  • At the page level, with the loading.tsx file (which creates for you).

  • At the component level, with for more granular control.

  1. Streaming a whole page with loading.tsx

In the /app/dashboard folder, create a new file called loading.tsx

put this there:

export default function Loading() { return

Loading...
; }

Refresh http://localhost:3000/dashboard, and you should now see

loading.tsx is a special Next.js file built on top of React Suspense

SIDE NOTE: Suspense is a built-in feature of React, not a separate library and not created by Next.js. Yes, Next.js uses React Suspense under the hood. When you create a loading.tsx file in a route, Next.js wraps your components in a boundary automatically.

loading.tsx is a special Next.js file built on top of React Suspense. It allows you to create fallback UI to show as a replacement while page content loads.

Since is static, it's shown immediately. The user can interact with while the dynamic content is loading. (this is called interruptable navigation).

Let's show a loading skeleton instead of the Loading… text.

  1. Adding loading skeletons

A loading skeleton is a simplified version of the UI.

use them as a placeholder (or fallback) to indicate to users that the content is loading

Any UI you add in loading.tsx will be embedded as part of the static file, and sent first. Then, the rest of the dynamic content will be streamed from the server to the client. Since is static, it's shown immediately. The user can interact with while the dynamic content is loading.

Inside your loading.tsx file, import a new component called :

Then, refresh http://localhost:3000/dashboard, and you should now see the skeleton of the UI before the whole thing actually renders

Fixing the loading skeleton bug with route groups:

Right now, your loading skeleton will apply to the invoices.

Since loading.tsx is a level higher than /invoices/page.tsx and /customers/page.tsx in the file system, it's also applied to those pages. EXAMPLE: Path Will show app/dashboard/loading.tsx: /dashboard ✅ /dashboard/invoices ✅ /dashboard/customers ✅ /dashboard/something-else ✅ / (root) ❌ /invoices (if exists at root) ❌

We can change this with Route Groups. Create a new folder called /(overview) inside the dashboard folder. INCLUDE PERENTHESIS () Then, move your loading.tsx and page.tsx files from dashboard root inside the overview folder

SIDE NOTE: If you dont include () your website to crash at dashboard route.

Now, the loading.tsx file will only apply to your dashboard overview page.

Route groups allow you to organize files into logical groups without affecting the URL path structure

When you create a new folder using parentheses (), the name won't be included in the URL path. So /dashboard/(overview)/page.tsx becomes /dashboard.

Here, you're using a route group to ensure loading.tsx only applies to your dashboard overview page.

you can also use route groups to separate your application into sections (e.g. (marketing) routes and (shop) routes) or by teams for larger applications.

Streaming a component:

So far, you're streaming a whole page

But you can also be more granular and stream specific components using React Suspense

is a React component that lets you "pause" part of your UI while waiting for async content to load, like server-rendered components or data fetching.

When you wrap a component in , React will show the fallback component while waiting for that component to load/render.

the slow data request, fetchRevenue(), this is the request that is slowing down the whole page. Instead of blocking your whole page, you can use Suspense to stream only this component and immediately show the rest of the page's UI.

move the data fetch to the component, let's update the code to see what that'll look like:

First delete all instances of fetchRevenue() and its data from /dashboard/(overview)/page.tsx

REMOVE: fetchRevenue from import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data';

const revenue = await fetchRevenue();

{ }

Second import from React, and wrap it around . You can pass it a fallback component called . The RevenueChartSkeleton will render while the rest of the page loads then the condition is met and then gets rendered

ADD: import { Suspense } from 'react'; import { RevenueChartSkeleton } from '@/app/ui/skeletons';

<Suspense fallback={}>

Finally, update the component to fetch its own data and remove the prop passed to it:

In /app/ui/dashboard/revenue-chart.tsx

REMOVE:

import { Revenue } from '@/app/lib/definitions';

PROPS FROM RevenueChart() -->> from this: export default async function RevenueChart({ revenue, }: { revenue: Revenue[]; }) TO THIS: export default async function RevenueChart() { // Make component async, remove the props

ADD: import { fetchRevenue } from '@/app/lib/data';

export default async function RevenueChart() // Make component async, remove the props

const revenue = await fetchRevenue(); // Fetch revenue inside the component add inside the function block for RevenueChart()

Now refresh the page, you should see the dashboard information almost immediately, while a fallback skeleton is shown for :

Practice: Streaming

ANSWER:

DELETE:

FROM dashboard/(OVERVIEW)

fetchLatestInvoices from ->> import { fetchLatestInvoices, fetchCardData} from '@/app/lib/data';

const latestInvoices = await fetchLatestInvoices();

{}

FROM dashboard/LATEST-INVOICES

import { LatestInvoice } from '@/app/lib/definitions';

remove Props in the LatestInvoices function { latestInvoices, }: { latestInvoices: LatestInvoice[]; }

from page.tsx

import {fetchCardData} from '@/app/lib/data';

ADD:

FROM dashboard/(OVERVIEW)

LatestInvoicesSkeleton to ->> import { RevenueChartSkeleton, LatestInvoicesSkeleton } from '@/app/ui/skeletons';

}>

FROM dashboard/LATEST-INVOICES

import { fetchLatestInvoices } from '@/app/lib/data';

inside LatestInvoices() ->> const latestInvoices = await fetchLatestInvoices();

  1. What Next.js Route Groups are, and when you might use them.

Grouping components:

now you need to wrap the components in Suspense. You can fetch data for each individual card, but this could lead to a popping effect as the cards load in, this can be visually jarring for the user.

So, to create more of a staggered effect, you can group the cards using a wrapper component. This means the static will be shown first, followed by the cards, etc.

ADD:

to /app/dashboard/(overview)/page.tsx:

import CardWrapper from '@/app/ui/dashboard/cards';

import { RevenueChartSkeleton, LatestInvoicesSkeleton, CardsSkeleton, } from '@/app/ui/skeletons';

<Suspense fallback={}>

Then /app/ui/dashboard/cards.tsx:

import { fetchCardData } from '@/app/lib/data';

export default async function CardWrapper() { const { numberOfInvoices, numberOfCustomers, totalPaidInvoices, totalPendingInvoices, } = await fetchCardData();

SIDE NOTE:

BECUASE THE CARD COMPONENT HAS MULTIPLE RETURN WHILE THE CHART AND LATEST-INVOICES DOESNT THATS THE REASON CARDS NEEDS A WRAPPER OVER THE OTHERS

Real Reason: before i had all those 's in the page.tsx and by moving the data collection and rendering to a seperate component it wraps them

With CardWrapper:

✅ You fetch once, render together

✅ The Suspense fallback applies to the group as a unit

✅ Clean UI with a single skeleton

  1. Deciding where to place your Suspense boundaries

Where you place your Suspense boundaries will depend on a few things:

  1. How you want the user to experience the page as it streams.

  2. What content you want to prioritize.

  3. If the components rely on data fetching.

With these in mind would you change the current dashboard/page.tsx?

Don't worry. There isn't a right answer.

You could stream the whole page like we did with loading.tsx... but that may lead to a longer loading time if one of the components has a slow data fetch.

You could stream every component individually... but that may lead to UI popping into the screen as it becomes ready.

You could also create a staggered effect by streaming page sections. But you'll need to create wrapper components.

In general, it's good practice to move your data fetches down to the components that need it, and then wrap those components in Suspense. But there is nothing wrong with streaming the sections or the whole page if that's what your application needs. experiment with Suspense and see what works best, it's a powerful API that can help you create more delightful user experiences.

CHAPTER 10: Partial Prerendering

So far, you've learned about static and dynamic rendering, and how to stream dynamic content that depends on data.

Now, let's learn how to combine static rendering, dynamic rendering, and streaming in the same route with Partial Prerendering (PPR).

PPR is only available with the Next.js canary releases (next@canary), not in the stable version of Next.js. We do not yet recommend using Partial Prerendering in production.

First pnpm install next@canary

Topics we’ll cover:

  1. What Partial Prerendering is.

  2. How Partial Prerendering works.

SIDE NOTE:

most routes are not fully static or dynamic. For example, consider an ecommerce site. You might want to statically render the majority of the product information page, but you may want to fetch the user's cart and recommended products dynamically, this allows you to show personalized content to your users.

currently all the doms except the are dynamic

  1. What is Partial Prerendering?

Partial Prerendering is still considered experimental and shouldnt be used in production – but it is a new rendering model that allows you to combine the benefits of static and dynamic rendering in the same route.

For example:

When a user visits a route:

-Lets say you have a static route shell that includes the navbar and product information this is served right away, ensuring a fast initial load.

-Then shell leaves holes where dynamic content like the cart and recommended products will load asynchronously.

-These async holes are streamed in parallel, reducing the overall load time of the page.

  1. How does Partial Prerendering work?

Partial Prerendering uses React's Suspense (which you learned about in the previous chapter) to defer rendering parts of your application until some condition is met (e.g. data is loaded).

The Suspense fallback is embedded into the initial HTML(skeleton files) file along with the static content.

At build time (or during revalidation), the static content is prerendered to create a static shell.

The rendering of dynamic content is postponed until the user requests the route.

Wrapping a component in Suspense doesn't make the component itself dynamic, but rather Suspense is used as a boundary between your static and dynamic code.

Implementing Partial Prerendering:

Enable PPR for your Next.js app by adding the ppr option to your next.config.ts file:

In next.config.ts add

experimental: { ppr: 'incremental' }

The 'incremental' value allows you to adopt PPR for specific routes.

Next, add the experimental_ppr segment config option to your dashboard layout:

In /app/dashboard/layout.tsx add

That's it. You may not see a difference in your application in development, but you should notice a performance improvement in production.

The great thing about Partial Prerendering is that you don't need to change your code to use it.

As long as you're using Suspense to wrap the dynamic parts of your route, Next.js will know which parts of your route are static and which are dynamic.

You can now revert these changes and move on to the next chapter.

RECAP: you've done a few things to optimize data fetching in your application so far:

-Created a database in the same region as your application code to reduce latency between your server and database.

-Fetched data on the server with React Server Components. This allows you to keep expensive data fetches and logic on the server, reduces the client-side JavaScript bundle, and prevents your database secrets from being exposed to the client.

-Used SQL to only fetch the data you needed, reducing the amount of data transferred for each request and the amount of JavaScript needed to transform the data in-memory.

-Parallelize data fetching with JavaScript - where it made sense to do so. Implemented Streaming to prevent slow data requests from blocking your whole page, and to allow the user to start interacting with the UI without waiting for everything to load.

-Move data fetching down to the components that need it, thus isolating which parts of your routes should be dynamic.

Chapter 11 Adding Search and Pagination

Now let's move on to the /invoices page, and learn how to add search and pagination.

Here are the topics we’ll cover:

1.Learn how to use the Next.js APIs: useSearchParams, usePathname, and useRouter.

2.Implement search and pagination using URL search params.

1.Learn how to use the Next.js APIs: useSearchParams, usePathname, and useRouter

Inside your /dashboard/invoices/page.tsx file, paste the following code:

import Pagination from '@/app/ui/invoices/pagination'; import Search from '@/app/ui/search'; import Table from '@/app/ui/invoices/table'; import { CreateInvoice } from '@/app/ui/invoices/buttons'; import { lusitana } from '@/app/ui/fonts'; import { InvoicesTableSkeleton } from '@/app/ui/skeletons'; import { Suspense } from 'react';

export default async function Page() { return (

<h1 className={${lusitana.className} text-2xl}>Invoices

{/* <Suspense key={query + currentPage} fallback={}> /}
{/ */}
); }

SIDE NOTE:

  1. allows users to search for specific invoices.

2. allows users to navigate between pages of invoices.

3.

displays the invoices.

Search functionality will span the client and the server.

When a user searches for an invoice on the client, the URL params will be updated, data will be fetched on the server, and the table will re-render on the server with the new data.

Why use URL search params?

you'll be using URL search params to manage the search state

benefits of implementing search with URL params

Bookmarkable and shareable URLs: Since the search parameters are in the URL, users can bookmark the current state of the application, including their search queries and filters, for future reference or sharing.

Server-side rendering: URL parameters can be directly consumed on the server to render the initial state, making it easier to handle server rendering.

Analytics and tracking: Having search queries and filters directly in the URL makes it easier to track user behavior without requiring additional client-side logic.

Next.js client hooks that you'll use to implement the search functionality:

useSearchParams Allows you to access the parameters of the current URL. For example, the search params for this URL /dashboard/invoices?page=1&query=pending would look like this: {page: '1', query: 'pending'}.

usePathname Lets you read the current URL's pathname. For example, for the route /dashboard/invoices, usePathname would return '/dashboard/invoices'.

useRouter Enables navigation between routes within client components programmatically. There are multiple methods you can use.

implementation steps:

  1. Capture the user's input.

  2. Update the URL with the search params.

  3. Keep the URL in sync with the input field.

  4. Update the table to reflect the search query.

  5. Capture the user's input

Go into the Component (/app/ui/search.tsx)

SIDE NOTE: This is a Client Component, which means you can use event listeners and hooks.

- is an HTML element used to create an interactive input field where users can enter text, numbers, or other data — in this case, it's used as a search field in the UI.

Create a new handleSearch function, and add an onChange listener to the element. onChange will invoke handleSearch whenever the input value changes.

declare:

function handleSearch(term: string) { console.log(term);

ADD:

onChange={(e) => { handleSearch(e.target.value); }}

SIDENOTE: The variables passed in this code operate like this:

Let's say the user types: apple

Then the onChange event listener gets triggered

onChange is a React prop that registers an event listener for the input’s "change" event.You assign it a function that will be called automatically when the input changes.

(e) = the parameter: a variable that will hold the event object

e is an object that React gives you — it's created from a class called SyntheticEvent, which wraps a native DOM event (like an input or click event).

It has properties like .target, .type, .preventDefault(), etc.

e.target.value → is the current value of that input field (e.g., "apple")

The class itself: class SyntheticEvent { target: HTMLElement; type: string; preventDefault: () => void; // ... many more }

check it works by opening the dashboard and selecting invoices and type into the search field. You should see the search term logged to the browser console dev tool.

  1. Update the URL with the search params

first, import the useSearchParams hook from next/navigation and assign it to a variable:

import { useSearchParams } from 'next/navigation';

const searchParams = useSearchParams();

Then, create handleSearch(), and inside that create a new URLSearchParams instance using your new searchParams variable:

function handleSearch(term: string) { const params = new URLSearchParams(searchParams); }

URLSearchParams is a Web API that provides utility methods for manipulating the URL query parameters.

Instead of creating a complex string literal, you can use it to get the params string like ?page=1&query=a.

Next, set the params string based on the user’s input. If the input is empty, you want to delete it:

if (term) {
  params.set('query', term);
} else {
  params.delete('query');
}

Now, that you have the query string. You can use Next.js's useRouter and usePathname hooks to update the URL:

Import useRouter and usePathname from 'next/navigation', and use the replace method from useRouter() inside handleSearch:

import { useSearchParams, usePathname, useRouter } from 'next/navigation';

in Search() put const pathname = usePathname(); const { replace } = useRouter();

in handleSearch() put replace(${pathname}?${params.toString()});

breakdown of what's happening:

${pathname} is the current path, in your case, "/dashboard/invoices".

As the user types into the search bar, params.toString() translates this input into a URL-friendly format.

replace(${pathname}?${params.toString()}) updates the URL with the user's search data. For example, /dashboard/invoices?query=lee if the user searches for "Lee".

The URL is updated without reloading the page, thanks to Next.js's client-side navigation (which you learned about in the chapter on navigating between pages.

  1. Keeping the URL and input in sync

To ensure the input field is in sync with the URL and will be populated when sharing, you can pass a defaultValue to input by reading from searchParams:

In Search() put defaultValue={searchParams.get('query')?.toString()}

SIDE NOTE: defaultValue vs. value / Controlled vs. Uncontrolled If you're using state to manage the value of an input, you'd use the value attribute to make it a controlled component. This means React would manage the input's state. However, since you're not using state, you can use defaultValue. This means the native input will manage its own state. This is okay since you're saving the search query to the URL instead of state.

  1. Updating the table

Finally, you need to update the table component to reflect the search query.

First, in /app/dashboard/invoices/page.tsx

Page components accept a prop called searchParams, so you can pass the current URL params to the

component.

add these props and declare variables for them to Page() export default async function Page(props: { searchParams?: Promise<{ query?: string; page?: string; }>; }) { const searchParams = await props.searchParams; const query = searchParams?.query || ''; const currentPage = Number(searchParams?.page) || 1;

Then uncomment the in Page()

SIDE NOTE If you navigate to the

Component, you'll see that the two props, query and currentPage, are passed to the fetchFilteredInvoices() function which returns the invoices that match the query.

With these changes in place, go ahead and test it out. If you search for a term, you'll update the URL, which will send a new request to the server, data will be fetched on the server, and only the invoices that match your query will be returned

the url in the browser now looks like this when i type my name http://localhost:3000/dashboard/invoices?query=richard

The console shows constent updating due to the event handler function handleSearch(term: string) {...}

Best practice: Debouncing

You've implemented search with Next.js! But there's something you can do to optimize it.

Inside your handleSearch function, add the following console.log:

console.log(Searching... ${term});

Then type "Delba" into your search bar and check the console in dev tools. console should show: Searching... D Searching... De Searching... Del Searching... Delb Searching... Delba

You're updating the URL on every keystroke, and therefore querying your database on every keystroke!

This isn't a problem as our application is small, but imagine if your application had thousands of users, each sending a new request to your database on each keystroke.

Debouncing is a programming practice that limits the rate at which a function can fire. In our case, you only want to query the database when the user has stopped typing.

How Debouncing Works:

Trigger Event: When an event that should be debounced (like a keystroke in the search box) occurs, a timer starts.

Wait: If a new event occurs before the timer expires, the timer is reset.

Execution: If the timer reaches the end of its countdown, the debounced function is executed.

You can implement debouncing in a few ways, including manually creating your own debounce function. To keep things simple, we'll use a library called use-debounce.

Install use-debounce:

pnpm i use-debounce

In your Component, import a function called useDebouncedCallback:

in /app/ui/search.tsx

remove:

function handleSearch(term: string)

Then ADD:

// ... import { useDebouncedCallback } from 'use-debounce';

// Inside the Search Component... const handleSearch = useDebouncedCallback((term) => {

}, 300);

SIDE NOTE: After these changes the book says the DB won't query until the time requirment is met but it's still executing after every keystroke

I read if you just put term instead of term:string then "You're creating the URLSearchParams object — but you're not assigning it to a variable, so the rest of the code (params.set(...), etc.) will fail because params is undefined."

Now it should work.

  1. Adding pagination

After introducing the search feature, you'll notice the table displays only 6 invoices at a time.

This is because the fetchFilteredInvoices() function in data.ts returns a maximum of 6 invoices per page.

Adding pagination allows users to navigate through the different pages to view all the invoices.

you can implement pagination using URL params, just like you did with search.

Navigate to the component in /app/dashboard/ui/invoices/pagenation and you'll notice that it's a Client Component

You don't want to fetch data on the client as this would expose your database secrets (remember, you're not using an API layer).

Instead, you can fetch the data on the server, and pass it to the component as a prop.

Then go to /app/dashboard/invoices/page.tsx

import a new function called fetchInvoicesPages and pass the query from searchParams as an argument:

import { fetchInvoicesPages } from '@/app/lib/data';

const totalPages = await fetchInvoicesPages(query);

fetchInvoicesPages returns the total number of pages based on the search query.

So if there are 12 invoices that match the search query, and each page displays 6 invoices, then the total number of pages would be 2.

Next, pass the totalPages prop to the component

in /app/dashboard/invoices/page.tsx

Add:

Now go to the component /app/ui/invoices/pagination.tsx uncomment the fucntion so you can use it and import the usePathname and useSearchParams hooks:

import { usePathname, useSearchParams } from 'next/navigation';

const pathname = usePathname();

const searchParams = useSearchParams();

const currentPage = Number(searchParams.get('page')) || 1;

Next, create a new function inside the Component called createPageURL.

you'll use URLSearchParams to set the new page number, and pathName to create the URL string.

in /app/ui/invoices/pagination.tsx add:

const createPageURL = (pageNumber: number | string) => { const params = new URLSearchParams(searchParams); params.set('page', pageNumber.toString()); return ${pathname}?${params.toString()}; };

Here's a breakdown of what's happening:

createPageURL creates an instance of the current search parameters.

Then, it updates the "page" parameter to the provided page number.

Finally, it constructs the full URL using the pathname and updated search parameters.

The next change is when the user types a new search query, you want to reset the page number to 1. You can do this by updating the handleSearch function in your component

in /app/ui/search.tsx add:

params.set('page', '1');

Congratulations! You've just implemented search and pagination using URL search params and Next.js APIs.

in this chapter:

You've handled search and pagination with URL search parameters instead of client state.

You've fetched data on the server.

You're using the useRouter router hook for smoother, client-side transitions.

chapter 12 Mutating Data

Let's continue working on the Invoices page by adding the ability to create, update, and delete invoices!

Here are the topics we’ll cover:

  1. What React Server Actions are and how to use them to mutate data.

  2. How to work with forms and Server Components.

  3. Best practices for working with the native FormData object, including type validation.

  4. How to revalidate the client cache using the revalidatePath API.

  5. How to create dynamic route segments with specific IDs.

  6. What are Server Actions and how to use them to mutate data

React Server Actions allow you to run asynchronous code directly on the server. ////////////////////////////////////////// SIDENOTE: console.log("1"); setTimeout(() => { console.log("2"); }, 1000); // wait 1 second console.log("3");

output = 132 becuase async wont wait fro the line to execute after 1 sec before moving on

VS defualt function or sycronise

console.log("1");

setTimeout(() => { console.log("2"); }, 1000); // wait 1 second

console.log("3");

output = 123

You cannot make a Client Component async only server compnents END SIDENOTE. //////////////////////////////////

They eliminate the need to create API endpoints to mutate your data.

Security is a top priority for web applications, as they can be vulnerable to various threats. This is where Server Actions come in.

They include features like encrypted closures, strict input checks, error message hashing, host restrictions, and more

Using forms with Server Actions:

you can use the action attribute in the element to invoke actions.

The action will automatically receive the native FormData object, containing the captured data.

For example:

// Server Component export default function Page() { // Action async function create(formData: FormData) { 'use server';

// Logic to mutate data...

}

// Invoke the action using the "action" attribute return ...; }

invoking a Server Action within a Server Component is progressive enhancement - forms work even if JavaScript has not yet loaded on the client. For example, without slower internet connections.

Server Actions are also deeply integrated with Next.js caching

When a form is submitted through a Server Action, not only can you use the action to mutate data, but you can also revalidate the associated cache using APIs like revalidatePath and revalidateTag.

  1. How to work with forms and Server Components.

Creating an invoice:

Here are the steps you'll take to create a new invoice:

  1. Create a form to capture the user's input.

  2. Create a Server Action and invoke it from the form.

  3. Inside your Server Action, extract the data from the formData object.

  4. Validate and prepare the data to be inserted into your database.

  5. Insert the data and handle any errors. Revalidate the cache and redirect the user back to invoices page.

  6. Create a new route and form

First, inside the /invoices folder, add a new route segment called /create with a page.tsx file

You'll be using this route to create new invoices. Inside your page.tsx file, paste the following code, then spend some time studying it:

import Form from '@/app/ui/invoices/create-form'; import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; import { fetchCustomers } from '@/app/lib/data';

export default async function Page() { const customers = await fetchCustomers();

return (

<Breadcrumbs breadcrumbs={[ { label: 'Invoices', href: '/dashboard/invoices' }, { label: 'Create Invoice', href: '/dashboard/invoices/create', active: true, }, ]} /> ); }

This page is a Server Component that fetches customers and passes it to the component

Navigate to the component, and you'll see that the form @/app/ui/invoices/create-form it has:

one (dropdown) element with a list of customers. one element for the amount with type="number".

two elements for the status with type="radio".

one button with type="submit".

  1. Create a Server Action

since the UI is now settup lets create a Server Action that is going to be called when the form is submitted.

Navigate to your lib/ directory and create a new file named actions.ts

At the top of this file, add the React use server directive:

By adding the 'use server', you mark all the exported functions within the file as Server Actions.

These server functions can then be imported and used in Client and Server components

Any functions included in this file that are not used will be automatically removed from the final application bundle.

SIDENOTE: You can also write Server Actions directly inside Server Components by adding "use server" inside the action. END SIDENOTE.

In your actions.ts file, create a new async function that accepts formData:

'use server';

export async function createInvoice(formData: FormData) {}

Next, in your component /app/ui/invoices/create-form.tsx, import the createInvoice from your actions.ts file. Add a action attribute to the element, and call the createInvoice action:

import { createInvoice } from '@/app/lib/actions';

SIDENOTE: In HTML, you'd pass a URL to the action attribute. This URL would be the destination where your form data should be submitted (usually an API endpoint).

However, in React, the action attribute is considered a special prop - meaning React builds on top of it to allow actions to be invoked.

Behind the scenes, Server Actions create a POST API endpoint. This is why you don't need to create API endpoints manually when using Server Actions. END SIDENOTE.

  1. Extract the data from formData

In your actions.ts file, you'll need to extract the values of formData, there are a couple of methods you can use

For this example, let's use the .get(name) method:

in /app/lib/actions.ts ADD

export async function createInvoice(formData: FormData) { const rawFormData = { customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status'), }; // Test it out: console.log(rawFormData); }

SIDENOTE:

If you're working with forms that have many fields, you may want to consider using the entries() method with JavaScript's Object.fromEntries().

END SIDENOTE.

To check everything is connected correctly, try out the form. After submitting, you should see the data you just entered into the form logged in your terminal not the browser becuase it uses 'use server'. This is becuase the forms action component is a 'use server' and VScode is acting as the server so the component's console.log(rawFormData); executes on the server not the browser

Now that your data is in the shape of an object, it'll be much easier to work with.

  1. Validate and prepare the data

Before sending the form data to your database, you want to ensure it's in the correct format and with the correct types

your invoices table /app/lib/definitions.ts expects data in the following format:

export type Invoice = { id: string; // Will be created on the database customer_id: string; amount: number; // Stored in cents status: 'pending' | 'paid'; date: string; };

Type validation and coercion with customer_id, amount, and status from the form.

First You'll notice that the console.log inside your action:

console.log(typeof rawFormData.amount);

The data shown such as amount is of type string and not number. This is because input elements with type="number" actually return a string, not a number!

To handle type validation, you have a few options.

Manually validate types, using a type validation library can save you time and effort.

For your example, we'll use Zod, a TypeScript-first validation library that can simplify this task for you.

In your actions.ts file, import Zod and define a schema that matches the shape of your form object.

This schema will validate the formData before saving it to a database. Specifically set to coerce (change) from a string to a number while also validating its type.

In /app/lib/actions.ts add:

import { z } from 'zod';

const FormSchema = z.object({ id: z.string(), customerId: z.string(), amount: z.coerce.number(), status: z.enum(['pending', 'paid']), date: z.string(), });

const CreateInvoice = FormSchema.omit({ id: true, date: true });

Then in /app/lib/actions.ts can then pass your rawFormData to CreateInvoice to validate the types:

(formData: FormData) { const { customerId, amount, status } = CreateInvoice.parse({

Change rawFormData -> to formData

It's usually good practice to store monetary values in cents in your database to eliminate JavaScript floating-point errors and ensure greater accuracy.

Let's convert the amount into cents:

in /app/lib/actions.ts add:

const amountInCents = amount * 100;

Finally, let's create a new date with the format "YYYY-MM-DD" for the invoice's creation date

in /app/lib/actions.ts add:

  1. Inserting the data into your database

Now that you have all the values you need for your database, you can create an SQL query to insert the new invoice into your database and pass in the variables:

in /app/lib/actions.ts add:

import postgres from 'postgres';

const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });

await sqlINSERT INTO invoices (customer_id, amount, status, date) VALUES (${customerId}, ${amountInCents}, ${status}, ${date});

SIDE NOTE:

Right now, we're not handling any errors. We'll talk about this in the next chapter. For now, let's move on to the next step.

  1. Revalidate and redirect

Next.js has a client-side router cache that stores the route segments in the user's browser for a time

long with prefetching, this cache ensures that users can quickly navigate between routes while reducing the number of requests made to the server

Since you're updating the data displayed in the invoices route, you want to clear this cache and trigger a new request to the server

do this with the revalidatePath function from Next.js:

in /app/lib/actions.ts add:

import { revalidatePath } from 'next/cache';

revalidatePath('/dashboard/invoices');

Once the database has been updated, the /dashboard/invoices path will be revalidated, and fresh data will be fetched from the server

At this point, you also want to redirect the user back to the /dashboard/invoices page. You can do this with the redirect function from Next.js:

in /app/lib/actions.ts add:

import { redirect } from 'next/navigation';

redirect('/dashboard/invoices');

Congratulations! You've just implemented your first Server Action. Test it out by adding a new invoice, if everything is working correctly

Updating an invoice

The updating invoice form is similar to the create an invoice form, except you'll need to pass the invoice id to update the record in your database

hese are the steps you'll take to update an invoice:

  1. Create a new dynamic route segment with the invoice id.

  2. Read the invoice id from the page params

  3. Fetch the specific invoice from your database.

  4. Pre-populate the form with the invoice data.

  5. Update the invoice data in your database.

  6. Create a Dynamic Route Segment with the invoice id

Next.js allows you to create Dynamic Route Segments when you don't know the exact segment name and want to create routes based on data

This could be blog post titles, product pages, etc.

You can create dynamic route segments by wrapping a folder's name in square brackets. For example, [id], [post] or [slug].

In your /invoices folder, create a new dynamic route called [id], then a new route called edit with a page.tsx file. Your file structure should look like this:

Don't delete anything this is a new route under invoices your creating

/invoices(folder)/id/edit(folder)/page.tsx(file)

In your

component /app/ui/invoices/table.tsx, notice there's a button that receives the invoice's id from the table records.

Navigate to your component, and update the href of the Link to accept the id prop. You can use template literals to link to a dynamic route segment:

in /app/ui/invoices/buttons.tsx add:

href={/dashboard/invoices/${id}/edit}

take out:

href="/dashboard/invoices"

  1. Read the invoice id from page params

now back in /app/dashboard/invoices/[id]/edit/page.tsx add:

import Form from '@/app/ui/invoices/edit-form'; import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; import { fetchCustomers } from '@/app/lib/data';

export default async function Page() { return (

<Breadcrumbs breadcrumbs={[ { label: 'Invoices', href: '/dashboard/invoices' }, { label: 'Edit Invoice', href: /dashboard/invoices/${id}/edit, active: true, }, ]} /> ); }

Notice how it's similar to your /create invoice page, except it imports a different form (from the edit-form.tsx file).

This form should be pre-populated with a defaultValue for the customer's name, invoice amount, and status.

To pre-populate the form fields, you need to fetch the specific invoice using id.

In addition to searchParams, page components also accept a prop called params which you can use to access the id. Update your component to receive the prop:

in /app/dashboard/invoices/[id]/edit/page.tsx add:

export default async function Page(props: { params: Promise<{ id: string }> }) { const params = await props.params; const id = params.id;

take out:

export default async function Page() {

  1. Fetch the specific invoice

Import a new function called fetchInvoiceById and pass the id as an argument.

Import fetchCustomers to fetch the customer names for the dropdown.

You can use Promise.all to fetch both the invoice and customers in parallel:

in /dashboard/invoices/[id]/edit/page.tsx add:

import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data'

const [invoice, customers] = await Promise.all([ fetchInvoiceById(id), fetchCustomers(), ]);

You will see a temporary TypeScript error for the invoice prop in your terminal because invoice could be potentially undefined we will fix with error handling in the next chapter

Now, test that everything is wired correctly. Visit http://localhost:3000/dashboard/invoices and click on the Pencil icon to edit an invoice. After navigation, you should see a form that is pre-populated with the invoice details:

The URL should also be updated with an id as follows: http://localhost:3000/dashboard/invoice/uuid/edit

SIDENOTE:

UUIDs vs. Auto-incrementing Keys

We use UUIDs instead of incrementing keys (e.g., 1, 2, 3, etc.). This makes the URL longer; however, UUIDs eliminate the risk of ID collision, are globally unique, and reduce the risk of enumeration attacks - making them ideal for large databases.

However, if you prefer cleaner URLs, you might prefer to use auto-incrementing keys.

  1. Pass the id to the Server Action

pass the id to the Server Action so you can update the right record in your database.

You cannot pass the id as an argument like so:

in /app/ui/invoices/edit-form.tsx:

// Passing an id as argument won't work

Instead, you can pass id to the Server Action using JS bind

This will ensure that any values passed to the Server Action are encoded.

In /app/ui/invoices/edit-form.tsx add:

import { updateInvoice } from '@/app/lib/actions';

const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);

return {/* ... */};

SIDENOTE: Note: Using a hidden input field in your form also works (e.g. ). However, the values will appear as full text in the HTML source, which is not ideal for sensitive data.

Then, in your actions.ts file, create a new action, updateInvoice:

create route /app/lib/actions.ts then add:

just check file out to much text/code to put in notes

Similarly to the createInvoice action, here you are:

  1. Extracting the data from formData.

  2. Validating the types with Zod.

  3. Converting the amount to cents.

  4. Passing the variables to your SQL query.

  5. Calling revalidatePath to clear the client cache and make a new server request.

  6. Calling redirect to redirect the user to the invoice's page.

Deleting an invoice

To delete an invoice using a Server Action, wrap the delete button in a element and pass the id to the Server Action using bind:

In /app/ui/invoices/buttons.tsx add:

import { deleteInvoice } from '@/app/lib/actions';

const deleteInvoiceWithId = deleteInvoice.bind(null, id);

Then inside your actions.ts file, create a new action called deleteInvoice:

In /app/lib/actions.ts add:

export async function deleteInvoice(id: string) { await sqlDELETE FROM invoices WHERE id = ${id}; revalidatePath('/dashboard/invoices'); }

Since this action is being called in the /dashboard/invoices path, you don't need to call redirect. Calling revalidatePath will trigger a new server request and re-render the table.

In this chapter, you learned how to use Server Actions to mutate data. You also learned how to use the revalidatePath API to revalidate the Next.js cache and redirect to redirect the user to a new page.

Chapter 13 Handling Errors

Topics we’ll cover:

  1. How to use the special error.tsx file to catch errors in your route segments, and show a fallback UI to the user.

  2. How to use the notFound function and not-found file to handle 404 errors (for resources that don’t exist).

First, let's add JavaScript's try/catch statements to your Server Actions to allow you to handle errors gracefully.

in /app/lib/actions.ts add:

a new createInvoice and updateInvoice was added

  • the full code is in the file, the createInvoices component await sql' function got turned into a "try" if else style function flow *

SIDENOTE:

  1. try/catch error handling is used when you don’t know in advance if an error might occur.
  2. Wraps risky code in try, and if an exception is thrown, execution jumps immediately to catch.
  3. Lets you recover from unexpected runtime errors without crashing the program. Example: //If sql executes successfully → program moves on, and catch is skipped. If sql throws (e.g., bad SQL, DB down) → You don’t write code to “save” the error. The JavaScript runtime engine does that for you. It Immediately stops running the rest of the try block.Creates an exception object (often an Error instance).Passes it into the catch block’s variable (in this case, error). That’s why you can access it inside catch(error).

error is an actuall object try/catch is not a true false boolean its a control flow statement

Inside catch, you can:

  1. Log it: console.error(error);
  2. Inspect it: console.error(error.message);
  3. Handle it: redirect('/dashboard/invoices?error=1'); try { await sqlUPDATE invoices SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status} WHERE id = ${id}; } catch (error) { // We'll log the error to the console for now console.error(error); }

so it basically works by Code in try { ... } runs.

If anything throws while inside try, execution jumps to catch (error) { ... }.

If nothing throws, catch is skipped.

Next, What happens if there is an uncaught exception in your action? We can simulate this by manually throwing an error. For example, in the deleteInvoice action, throw an error at the top of the function:

add to deleteInvoices: throw new Error('Failed to Delete Invoice');

When you try to delete an invoice, you should see the error on localhost. When going to production, you want to more gracefully show a message to the user when something unexpected happens.

This is where Next.js error.tsx file comes in.

First remove the manually add erorr before moving on.

  1. Handling all errors with error.tsx

The error.tsx file can be used to define a UI boundary for a route segment.

It serves as a catch-all for unexpected errors and allows you to display a fallback UI to your users.

Example:

Inside your /dashboard/invoices folder, create a new file called error.tsx and paste the following code:

  • code in /dashboard/invoices/error.tsx *

go to deleteInvoice() in actions.tsx and uncomment the manual error throw

you'll notice about the code above:

  1. client component
  2. accepts the prop error, an instance of JavaScript's native Error object.
  3. accepts the prop reset, a function to reset the error boundary. When executed, the function will try to re-render the route segment.

summery: we've modularized the error capture using a error.tsx in the same way a page defualts to the nearest layout.tsx to get UI the page defualts to error.tsx to handle errors thrown.

  1. Handling 404 errors with the notFound function

Another way you can handle errors gracefully is by using the notFound function.

While error.tsx is useful for catching uncaught exceptions, notFound can be used when you try to fetch a resource that doesn't exist. So we used try catch to handle error we'll use notFound to handle a undefined resource

For example, visit http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit.

This is a fake UUID that doesn't exist in your database.

You'll immediately see error.tsx kicks in because this is a child route of /invoices where error.tsx is defined.

However, if you want to be more specific, you can show a 404 error to tell the user the resource they're trying to access hasn't been found.

You can confirm that the resource hasn't been found by going into your fetchInvoiceById function in data.ts, and console logging the returned invoice:

In /app/lib/data.ts add:

console.log(invoice); // Invoice is an empty array []

Now that you know the invoice doesn't exist in your database, let's use notFound to handle it

Go to /dashboard/invoices/[id]/edit/page.tsx, and import { notFound } from 'next/navigation':

import { notFound } from 'next/navigation';

if (!invoice) { notFound(); }

Then, to show error UI to the user, create a not-found.tsx file inside the /edit folder:

/dashboard/invoices/[id]/edit/not-found.tsx

Now in there add:

** code is in file **

Go to actions.tsx and comment the manual error throw becuase notFound will take precedence over error.tsx, so you can reach out for it when you want to handle more specific errors! then refresh the route, and you should now see 404 not found. http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit

Chapter 14 Improving Accessibility

we still need to discuss another piece of the puzzle: form validation

Lets see how to implement server-side validation with Server Actions, and how you can show form errors using React's useActionState hook - while keeping accessibility in mind!

Here are the topics we’ll cover:

  1. How to use eslint-plugin-jsx-a11y with Next.js to implement accessibility best practices.

  2. How to implement server-side form validation.

  3. How to use the React useActionState hook to handle form errors, and display them to the user.

  4. What is accessibility?

Accessibility refers to designing and implementing web applications that everyone can use, including those with disabilities. Such as keyboard navigation, semantic HTML, images, colors, videos, etc.

we'll discuss the accessibility features available in Next.js and some common practices to make your applications more accessible.

Lets use the ESLint accessibility plugin in Next.js

Next.js includes the eslint-plugin-jsx-a11y plugin in its ESLint config to help catch accessibility issues early

example, this plugin warns if you have images without alt text, use the aria-* and role attributes incorrectly, and more.

aria-* is Accessible Rich Internet Applications.

Whenever you see aria-*, it’s a placeholder for any ARIA attribute. Examples: aria-label="Close menu" - Provides a label for an element (used by screen readers).

If you would like to try this out, add next lint as a script in your package.json file:

In /package.json add:

"lint": "next lint"

Then run pnpm lint in your terminal:

The warnings you'll get pertain to undclared variables and unused card data. This is becuase ESLint is not just for accessibility. It’s a general code quality tool that checks for many different categories of problems.

If we wanted to test for aria warning go to /app/ui/invoices/table.tsx and remove the alt prop from the image. You can use your editor's search feature to quickly find the :

alt={${invoice.name}'s profile picture} // Delete this line

Then run pnpm lint and you should see:

./app/ui/invoices/table.tsx 45:25 Warning: Image elements must have an alt prop, either with meaningful text, or an empty string for decorative images. jsx-a11y/alt-text

Now lets discuss Improving form accessibility

There are three things we're already doing to improve accessibility in our forms:

  1. Semantic HTML: Using semantic elements (, , etc) instead of

    . This allows assistive technologies (AT) to focus on the input elements and provide appropriate contextual information to the user, making the form easier to navigate and understand.

  2. Labelling: Including and the htmlFor attribute ensures that each form field has a descriptive text label. This improves AT support by providing context and also enhances usability by allowing users to click on the label to focus on the corresponding input field.

  3. Focus Outline: The fields are properly styled to show an outline when they are in focus. This is critical for accessibility as it visually indicates the active element on the page, helping both keyboard and screen reader users to understand where they are on the form. You can verify this by pressing tab.

These practices lay a good foundation for making your forms more accessible to many users. However, they don't address form validation and errors.

  1. Form validation

Go to http://localhost:3000/dashboard/invoices/create, and submit an empty form.

You get an error! This is because you're sending empty form values to your Server Action. You can prevent this by validating your form on the client or the server.

Client-Side validation

There are a couple of ways you can validate forms on the client.

One way is to rely on the form validation provided by the browser by adding the required attribute to the and elements in your forms. For example: In /app/ui/invoices/create-form.tsx add: required -to the

Submit the form again. The browser will display a warning if you try to submit a form with empty values.

This approach is generally okay because some ATs support browser validation.

For now, delete the required attribute if you added it so we can test out server-side validation

  1. Server-Side validation/3. How to use the React useActionState hook

By validating forms on the server, you can:

  1. Ensure your data is in the expected format before sending it to your database.
  2. Reduce the risk of malicious users bypassing client-side validation.
  3. Have one source of truth for what is considered valid data.

In your create-form.tsx component, import the useActionState hook from react. Since useActionState is a hook, you will need to turn your form into a Client Component using "use client" directive:

in /app/ui/invoices/create-form.tsx add:

'use client';

// ... import { useActionState } from 'react';

Then Inside your Form Component /app/ui/invoices/create-form.tsx, for the useActionState hook use:

To store the state and formAction variables returned by the hook,

const [state, formAction] = useActionState(createInvoice, initialState);

The action prop tells the form where to send the data in HTML or what function to call in React/Next.js. Since we will use useActionState() to handle out form data we take the createInvoice inside your attribute and give that to the useActionState() and replace it with return variable formAction we created above,

return ...;

Now we have to intialize a initialState var for our useActionState() argument

The initialState can be anything you define, in this case, create an object with two empty keys: message and errors, and import the State type from your actions.ts file. State does not yet exist, but we will create it next:

in /app/ui/invoices/create-form.tsx add:

at top: import { createInvoice, State } from '@/app/lib/actions';

in form(): const initialState: State = { message: null, errors: {} };

Now your need to create the State in the actions file so create-form can import it

In your action.ts file, you can use Zod to validate form data. Update your FormSchema as follows:

In /app/lib/actions.ts change FormSchema to:

const FormSchema = z.object({ id: z.string(), customerId: z.string({ invalid_type_error: 'Please select a customer.', }), amount: z.coerce .number() .gt(0, { message: 'Please enter an amount greater than $0.' }), status: z.enum(['pending', 'paid'], { invalid_type_error: 'Please select an invoice status.', }), date: z.string(), });

This schema will now contain the variables needed to supply the return data to create-form that goes with the usActionState() and the const CreateInvoice = FormSchema.omit({ id: true, date: true }); variable we created in he action.ts file.

Next, update your createInvoice action to accept two parameters - prevState and formData

updating the schema improves validation and messages, but it doesn’t by itself tell your form UI what to render after submit (errors, messages, pending state). That’s what useActionState is for—and it expects your action to have the signature

to use useActionState, React needs a function that:

  1. receives the previous state,
  2. processes the submission (formData),
  3. returns a new state for the form to render (on error). Basically passing the state object back and forth from client & sever to be checked and used by each

A plain createInvoice(formData) can’t return UI state to the form in this model. Hence the required signature change.

in /app/lib/actions.ts add:

export type State = { errors?: { customerId?: string[]; amount?: string[]; status?: string[]; }; message?: string | null; };

export async function createInvoice(prevState: State, formData: FormData) { // ... }

prevState - contains the state passed from the useActionState hook. You won't be using it in the action in this example, but it's a required prop.

Then, change the Zod parse() function to safeParse():

In /app/lib/actions.ts add:

const validatedFields = CreateInvoice.safeParse({

safeParse() will return an object containing either a success or error field. This will help handle validation more gracefully without having put this logic inside the try/catch block.

parse vs safeParse (what changed)

parse():

On success → returns the parsed data. On failure → throws a ZodError. Consequence: you must use try/catch around validation to handle errors.

safeParse():

Never throws. Returns an object: { success: true, data } on success { success: false, error } on failure

Consequence, you can handle validation without try/catch and keep DB/other errors in their own try/catch.

useActionState can’t “handle” ZodError directly because it doesn’t work with exceptions.It works with returned state objects, so you convert ZodError → { errors, message } before returning. That’s why the State type and safeParse() are both introduced: they give you a structured, serializable way to pass validation feedback back to the client.

Now, Before sending the information to your database, check if the form fields were validated correctly using with a conditional:

In /app/lib/actions.ts add:

// If form validation fails, return errors early. Otherwise, continue. if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, message: 'Missing Fields. Failed to Create Invoice.', }; }

validatedFields is not in state — it’s a temporary server-side variable. You map its results(errors in the recieved form data) into the state object, which React then uses in your form.

If validatedFields isn't successful, we return the function early with the error messages from Zod mapped to the unique state variable that triggered it

Finally, since you're handling form validation separately, outside your try/catch block, you can return a specific message for any database errors, your final code should look like this

In /app/lib/actions.ts add to createInvoice():

// Prepare data for insertion into the database const { customerId, amount, status } = validatedFields.data; const amountInCents = amount * 100; const date = new Date().toISOString().split('T')[0];

To trigger the console log error to check if useActionState is working do this:

  1. Don’t select a customer from the dropdown. Since customerId is required, Zod will fail and return errors.customerId.

  2. Leave amount empty or type 0 Because of .gt(0), if amount is missing or 0, it will return errors.amount.

  3. Don’t pick a status. Since status is an enum, Zod will throw an error if neither “pending” nor “paid” is chosen.

Error you will get: GET /dashboard/invoices/create 200 in 1266ms { success: false, error: [Getter] }

Now let's use the new state sent to the clients side sent to by the server to display the errors in your form component using aria labels. Back in the create-form.tsx component, you can access the errors using the form state.

Add a ternary operator that checks for each specific error. For example, after the customer's field, you can add:

In /app/ui/invoices/create-form.tsx:

In the add aria-describedby="customer-error" in the middle add a new you'll see where, check code {state.errors?.customerId && state.errors.customerId.map((error: string) => ( {error} ))} Lets look at a simpler use of useActionState's abilities Example: {state.errors?.customerId && {state.errors.customerId}} Curly braces {} in JSX In React/JSX, curly braces let you embed JavaScript expressions inside HTML-like syntax. So everything between { ... } is evaluated as JS. state.errors?.customerId state is the object returned by useActionState. errors is an optional property inside state (that’s why you see the ?. optional chaining). customerId is a possible key inside errors, which would hold error messages related to the customer field. If errors is null or undefined, the ?. prevents a crash — the expression simply returns undefined. The && short-circuit In JS/TS, a && b means: If a is truthy, return b. If a is falsy, return a (and skip b). Here, if state.errors?.customerId exists and has a value, React will render the element. If not, nothing renders (it’s like returning null). {state.errors.customerId} If the condition passes, React renders a element. Inside it, you again use curly braces to inject the actual error text (the value of state.errors.customerId). Summary for above explanation: The useactionstate allows for functions like these becuase now there is a object stored on the client side with variables in the object(state) to check for like errors that trigger aria labels(error messages) these messa Final test, add ability to edit(edit-form.tsx) invoices & incorperate useActionState You'll need to: Add useActionState to your edit-form.tsx component. ANSWER: First declare import: import { useActionState } from 'react'; Next Bind the ID and wire the hook: const initialState: State = { message: null, errors: {} }; const updateInvoiceWithId = updateInvoice.bind(null, invoice.id); const [state, formAction] = useActionState(updateInvoiceWithId, initialState); finnally Use the returned action: Edit the updateInvoice action in actions.tsx to handle validation errors from Zod. First Derived update schema (top-level; not inside function): const UpdateInvoice = FormSchema.omit({ id: true, date: true }); Next Update/add signature for useActionState (id bound + prevState support): export async function updateInvoice( id: string, prevState: State, formData: FormData ) { Next Validate with safeParse and return errors: const validatedFields = UpdateInvoice.safeParse({ customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status'), }); if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, message: 'Missing Fields. Failed to Update Invoice.', }; } Finally Use validated data, handle DB error with a message: const { customerId, amount, status } = validatedFields.data; const amountInCents = amount * 100; try { await sqlUPDATE invoices SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status} WHERE id = ${id}; } catch { return { message: 'Database Error: Failed to Update Invoice.' }; } revalidatePath('/dashboard/invoices'); redirect('/dashboard/invoices'); Display the errors in your component, and add aria labels to improve accessibility in edit-form.tsx First Customer select with ARIA hookup: <select id="customer" name="customerId" defaultValue={invoice.customer_id} className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2" aria-describedby="customer-error" Next Customer error region: {state.errors?.customerId?.map((error) => ( {error} ))} Next Amount input with ARIA hookup:

Next Amount error region:

{state.errors?.amount?.map((error) => (

{error}

))}

Then Status group + error region:

{/* radios ... */}
{state.errors?.status?.map((error) => (

{error}

))}

CHAPTER 15 ADDING AUTHENTICATION In this chapter, you'll be adding authentication to your dashboard.

Here are the topics we’ll cover:

  1. What is authentication.

  2. How to add authentication to your app using NextAuth.js.

  3. How to use Middleware to redirect users and protect your routes.

  4. How to use React's useActionState to handle pending states and form errors.

  5. What is authentication?

It's how a system checks if the user is who they say they are.

A secure website often uses multiple ways to check a user's identity.

Example: after entering your username and password, the site may send a verification code to your device or use an external app like Google Authenticator. This 2-factor authentication (2FA) helps increase security.

Even if someone learns your password, they can't access your account without your unique token.

Authentication vs. Authorization

Authentication, is about making sure the user is who they say they are. You're proving your identity with something you have like a username and password.

Authorization, is the next step. Once a user's identity is confirmed, authorization decides what parts of the application they are allowed to use.

So, authentication checks who you are, and authorization determines what you can do or access in the application.

  1. How to add authentication to your app using NextAuth.js.

First Creating the login route

create a new route in your application /app/login/page.tsx and paste the following code:

{code in file}

notice the page imports , which you'll update later in the chapter. This component is wrapped with React because it will access information from the incoming request (URL search params).

NextAuth.js abstracts away much of the complexity involved in managing sessions, sign-in and sign-out, and other aspects of authentication.

While you can manually implement these features, the process can be time-consuming and error-prone.

NextAuth.js simplifies the process, providing a unified solution for auth in Next.js applications.

Setting up NextAuth.js:

Install NextAuth.js by running the following command in your terminal

pnpm i next-auth@beta

Here, you're installing the most updated or beta version of NextAuth.js, which is compatible with Next.js 14+ but is unstable for production we will cover the nextauth version 4 next which is the stable version

Next, generate a secret key for your application. This key is used to encrypt cookies, ensuring the security of user sessions.

You can do this by running the following command in your terminal:

macOS

openssl rand -base64 32

Windows

Go to this website generate one key https://generate-secret.vercel.app/32 then paste this in .env like this: . Example:

AUTH_SECRET=7hxkX9bN3ZkQkydX2Fg0P1pV4z2eX6jPa0eHgsa0pT4

Now lets add page options

Create an auth.config.ts file at the root of our project that exports an authConfig object

This object will contain the configuration options for NextAuth.js. For now, it will only contain the pages option:

In /auth.config.ts add:

import type { NextAuthConfig } from 'next-auth';

export const authConfig = { pages: { signIn: '/login', }, } satisfies NextAuthConfig;

SIDENOTE: Object” vs “Option”

Object → the technical JavaScript structure ({ key: value }).

Option → in config files/libraries, people often say “options” to mean “settings you can pass in.” here are the options for nextauth: pages: { signIn: '/login', signOut: '/logout', error: '/auth-error', newUser: '/welcome', }

You can use the pages option to specify the route for customer sign-in, sign-out, and error pages all handled by nextauth

This is not required, but by adding signIn: '/login' into our pages option, the user will be redirected to our custom login page, rather than the NextAuth.js default page. like if someone tried to access profile edit settings and they wern't logged in next auth captures and redirects them to login

  1. Protecting your routes with Next.js Middleware

Next, add the logic to protect your routes. This will prevent users from accessing the dashboard pages unless they are logged in.

In auth.config.ts add: callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); if (isOnDashboard) { if (isLoggedIn) return true; return false; // Redirect unauthenticated users to login page } else if (isLoggedIn) { return Response.redirect(new URL('/dashboard', nextUrl)); } return true; }, }, providers: [], // Add providers with an empty array for now } satisfies NextAuthConfig;

The authorized callback is used to verify if the request is authorized to access a page with Next.js Middleware.

It is called before a request is completed, and it receives an object with the auth and request properties.

The auth property contains the user's session, and the request property contains the incoming request.

The providers option is an array where you list different login options.

For now, it's an empty array to satisfy NextAuth config. You'll learn more about it in the Adding the Credentials provider section.

Next, you will need to import the authConfig object into a Middleware file. In the root of your project, create a file called middleware.ts and paste the following code:

In root add middleware.ts file then add:

import NextAuth from 'next-auth'; import { authConfig } from './auth.config';

export default NextAuth(authConfig).auth;

//the book gives you this export const config = { // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher matcher: ['/((?!api|_next/static|_next/image|.\.png$).)'], runtime: 'nodejs', };

//what is should be export const config = { matcher: ["/dashboard/:path*"], // ✅ only protects dashboard routes };

This is becuase To run Node.js runtime middleware, you’d need to self-host Next.js in Node, not deploy on Vercel Edge.That would let you use runtime: 'nodejs' and Node-only libraries in middleware.But in practice, the recommended pattern is: Keep middleware Edge-safe (just session check). Do Node work (bcrypt, DB) in API routes, server actions, or auth.ts.

Here you're initializing NextAuth.js with the authConfig object and exporting the auth property.

You're also using the matcher option from Middleware to specify that it should run on specific paths.

The advantage of employing Middleware for this task is that the protected routes will not even start rendering until the Middleware verifies the authentication, enhancing both the security and performance of your application.

Password hashing

It's good practice to hash passwords before storing them in a database.

Hashing converts a password into a fixed-length string of characters, which appears random, providing a layer of security even if the user's data is exposed.

When seeding your database, you used a package called bcrypt to hash the user's password before storing it in the database.

You will use it again later in this chapter to compare that the password entered by the user matches the one in the database.

However, you will need to create a separate file for the bcrypt package. This is because bcrypt relies on Node.js APIs not available in Next.js Middleware.

Create a new file in root called auth.ts that spreads your authConfig object

auth.ts is where the actual NextAuth logic gets initialized and exported for use everywhere else in your app.

Add to auth.ts:

import NextAuth from 'next-auth'; import { authConfig } from './auth.config';

export const { auth, signIn, signOut } = NextAuth({ ...authConfig, });

Difference between auth.config.ts and auth.ts

auth.config.ts

Holds the options for NextAuth.

It’s just a plain configuration object describing how NextAuth should behave (providers, pages, callbacks, etc.).

On its own, it doesn’t run any logic — it’s the “recipe” or “settings.”

auth.ts

Acts like the engine that initializes NextAuth using the config.

This file imports the config and creates the actual NextAuth instance.

From it, you get reusable exports:

    1.handlers → used in API routes (/api/auth/*), the actual functions Next.js calls for login, logout, callbacks, etc.

    2.auth → a helper used in middleware to check sessions (export { auth as middleware } from "./auth").

    3.signIn / signOut → client-side helpers you can call inside React components.

Now lets add the Credentials provider

You will need to add the providers option for NextAuth.js

Providers is an array where you list different login options such as Google or GitHub. Remember the empty array we left in auth.config

In this course, we will focus on using the Credentials provider only

The Credentials provider allows users to log in with a username and a password.

In auth.ts add:

import Credentials from 'next-auth/providers/credentials';

providers: [Credentials({})],

SIDENOTE: There are other alternative providers such as OAuth, google, or email. See the NextAuth.js docs for a full list of options

Adding the sign in functionality

You can use the authorize function to handle the authentication logic

Similarly to Server Actions, you can use zod to validate the email and password before checking if the user exists in the database:

in auth.ts add:

import { z } from 'zod';

providers: [ Credentials({ async authorize(credentials) { const parsedCredentials = z .object({ email: z.string().email(), password: z.string().min(6) }) .safeParse(credentials); }, }),

Now after we just validated the credentials, create a new getUser function that queries the user from the database

In auth.ts add:

import type { User } from '@/app/lib/definitions'; import bcrypt from 'bcrypt'; import postgres from 'postgres';

const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });

//this function was added entirely async function getUser(email: string): Promise<User | undefined> { try { const user = await sql<User[]>SELECT * FROM users WHERE email=${email}; return user[0]; } catch (error) { console.error('Failed to fetch user:', error); throw new Error('Failed to fetch user.'); } }

in the export = NextAuth() we add:

      .safeParse(credentials);

    if (parsedCredentials.success) {
      const { email, password } = parsedCredentials.data;
      const user = await getUser(email);
      if (!user) return null;
    }

    return null;
  },
}),

], });

Then, call bcrypt.compare to check if the passwords match:

In auth.ts add:

import postgres from 'postgres';

//inside if(parsedCredentials.succes) const user = await getUser(email); const passwordsMatch = await bcrypt.compare(password, user.password); if (passwordsMatch) return user;

if the passwords match you want to return the user, otherwise, return null to prevent the user from logging in:

console.log('Invalid credentials');

Updating the login form

Now you need to connect the auth logic with your login form

In your actions.ts file, create a new action called authenticate:

export async function authenticate( prevState: string | undefined, formData: FormData, ) { try { await signIn('credentials', formData); } catch (error) { if (error instanceof AuthError) { switch (error.type) { case 'CredentialsSignin': return 'Invalid credentials.'; default: return 'Something went wrong.'; } } throw error; } }

If there's a 'CredentialsSignin' error, you want to show an appropriate error message. You can learn about NextAuth.js errors in https://errors.authjs.dev/

Finally, in your login-form.tsx component, you can use React's useActionState to call the server action, handle form errors, and display the form's pending state:

In app/ui/login-form.tsx add:

'use client';

import { useActionState } from 'react'; import { authenticate } from '@/app/lib/actions'; import { useSearchParams } from 'next/navigation';

This will replace the old form element:

Add some arialabels:

aria-live="polite" aria-atomic="true"

New button replaces old:

    <input type="hidden" name="redirectTo" value={callbackUrl} />
    <Button className="mt-4 w-full" aria-disabled={isPending}>
      Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
    </Button>

now lets add our nextAuth:

{errorMessage && ( <>

{errorMessage}

</> )}

Finally lets Add the logout functionality

To add the logout functionality to , call the signOut function from auth.ts in your element:

in /ui/dashboard/sidenav.tsx add:

import { signOut } from '@/auth';

    <form
      action={async () => {
        'use server';
        await signOut({ redirectTo: '/' });
      }}
    >

Now try it out:

You should be able to log in and out of your application using the following credentials:

Email: user@nextmail.com Password: 123456

Chapter 16 Adding Metadata

The topics we’ll cover:

  1. What metadata is

  2. Types of metadata

  3. How to add an Open Graph image using metadata

  4. How to add a favicon using metadata

  5. What is metadata?

In web development, metadata provides additional details about a webpage.

Metadata is not visible to the users visiting the page. Instead, it works behind the scenes, embedded within the page's HTML, usually within the element.

This hidden information is crucial for search engines and other systems that need to understand your webpage's content better

Metadata plays a significant role in enhancing a webpage's SEO, making it more accessible and understandable for search engines and social media platforms

Proper metadata helps search engines effectively index webpages, improving their ranking in search results

Additionally, metadata like Open Graph improves the appearance of shared links on social media, making the content more appealing and informative for users

  1. Some common types of metadata

  2. Title Metadata: Responsible for the title of a webpage that is displayed on the browser tab. It's crucial for SEO as it helps search engines understand what the webpage is about

Example:

<title>Page Title</title>
  1. Description Metadata: This metadata provides a brief overview of the webpage content and is often displayed in search engine results.

Example:

  1. Keyword Metadata: This metadata includes the keywords related to the webpage content, helping search engines index the page

Example:

  1. Open Graph Metadata: This metadata enhances the way a webpage is represented when shared on social media platforms, providing information such as the title, description, and preview image
  1. Favicon Metadata: This metadata links the favicon (a small icon) to the webpage, displayed in the browser's address bar or tab
  1. Adding metadata

Next.js has a Metadata API that can be used to define your application metadata

There are two ways you can add metadata to your application:

  1. Config-based: Export a static metadata object or a dynamic generateMetadata function in a layout.js or page.js file

  2. File-based: Next.js has a range of special files that are specifically used for metadata purposes

File list example: favicon.ico, apple-icon.jpg, and icon.jpg: Utilized for favicons and icons

opengraph-image.jpg and twitter-image.jpg: Employed for social media images

robots.txt: Provides instructions for search engine crawling

sitemap.xml: Offers information about the website's structure

You have the flexibility to use these files for static metadata, or you can generate them programmatically within your project

With both these options, Next.js will automatically generate the relevant elements for your pages

  1. Favicon and Open Graph image

In your /public folder, you'll notice you have two images: favicon.ico and opengraph-image.jpg

Move these images to the root of your /app folder

After doing this, Next.js will automatically identify and use these files as your favicon and OG image

You can verify this by checking the element of your application in dev tools

sidenote: You can also create dynamic OG images using the ImageResponse constructor

Page title and descriptions:

You can also include a metadata object from any layout.js or page.js file to add additional page information like title and description

Any metadata in layout.js will be inherited by all pages that use it.

In your root layout, create a new metadata object with the following fields

In /app/layout.tsx add:

import { Metadata } from 'next';

export const metadata: Metadata = { title: 'Acme Dashboard', description: 'The official Next.js Course Dashboard, built with App Router.', metadataBase: new URL('https://next-learn-dashboard.vercel.sh'), };

Side Note: .tsx file can export both: React components (export default function Page() { … }) Metadata objects (export const metadata = { … })

Next.js will automatically add the title and metadata to your application

But what if you want to add a custom title for a specific page?

You can do this by adding a metadata object to the page itself

Metadata in nested pages will override the metadata in the parent

For example, in the /dashboard/invoices page, you can update the page title:

In /app/dashboard/invoices/page.tsx add:

import { Metadata } from 'next';

export const metadata: Metadata = { title: 'Invoices | Acme Dashboard', };

Now look at the tab the title is visable from invoice page

This works, but we are repeating the title of the application in every page. If something changes, like the company name, you'd have to update it on every page

Instead, you can use the title.template field in the metadata object to define a template for your page titles

This template can include the page title, and any other information you want to include

In your root layout, update the metadata object to include a template:

The %s in the template will be replaced with the specific page title.

Now, in your /dashboard/invoices page, you can add the page title:

In /app/dashboard/invoices/page.tsx add:

export const metadata: Metadata = { title: 'Invoices', };

Go to the websites /dashboard/invoices page and check the element. You should see the page title is now Invoices | Acme Dashboard.

Practice: Adding metadata Now that you've learned about metadata, practice by adding titles to your other pages:

/login page. /dashboard/ page. /dashboard/customers page. /dashboard/invoices/create page. /dashboard/invoices/[id]/edit page.

The Next.js Metadata API is powerful and flexible, giving you full control over your application's metadata. Here, we've shown you how to add some basic metadata, but you can add multiple fields, including keywords, robots, canonical, and more. Feel free to explore the documentation( https://nextjs.org/docs/app/api-reference/functions/generate-metadata ), and add any additional metadata you want to your application

to do continued learning now that you have full scope of nextjs use the links on this page:

https://nextjs.org/learn/dashboard-app/next-steps

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages