Build a Full-Stack Feedback Board

Build a multi-page feedback app with Next.js Server Actions and file-based routing.

Introduction

30 Second Summary

Every web app needs a way to collect user input and show it back. Think of any feedback form, comment section, or suggestion box you've ever used online.

In this project, you will build a full-stack feedback board using Next.js that lets users submit feedback through a form and view all submissions on a separate page.

What You'll Build

A multi-page feedback board application with a submission form, server-side data processing, and a display page, all built within a single Next.js project.

By the end of this project, you'll have:

  • A running Next.js app with file-based routing and a shared navigation layout.
  • A feedback submission form powered by Server Actions with no separate backend server needed.
  • A feedback display page that reads data directly on the server using Server Components and renders it to the browser.
  • Secret Mission: Add category filtering so users can view feedback by type.

Are there any prerequisites?

No prior experience with Next.js or React is required. This guide includes setup instructions for all required tools, including Node.js and VS Code.

Before We Start

Before We Start...

Set Up Your Next.js Project

To build a full-stack feedback board, you need a framework that handles both the user interface and the server logic in one project. Next.js does exactly this.

In this step, you will scaffold a brand-new Next.js project, start the development server, and confirm everything runs smoothly before building any features.

In this step, get ready to:
  • Create a Next.js project using the official scaffolding tool.
  • Start the development server and view the app in your browser.
  • Clean up the default page to start with a blank canvas.
Create your Next.js project

Next.js provides a command-line tool called create-next-app that generates a ready-to-run project with all the configuration done for you.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your Next.js project by running this command:
npx create-next-app@latest feedback-board --yes

What does this command do?

npx create-next-app@latest downloads and runs the official Next.js project generator.

feedback-board is the name of your project folder. The --yes flag accepts all the recommended defaults automatically: TypeScript, ESLint, Tailwind CSS, App Router, and Turbopack.

  • Wait for the installation to finish. You should see a success message in your terminal once all dependencies are installed.
  • Move into the new project folder by running:
cd feedback-board
  • Confirm the project was created by running:
ls
  • You should see files and folders including app, node_modules, package.json, and next.config.ts.

Command not found?

If you see an error about npx not being recognized, your Node.js installation may not be on your system PATH. Close and reopen your terminal, then try again.

Next.js 16 requires Node.js 20.9.0 or higher. Run node --version to check your version.

Help me troubleshoot my create-next-app setup

Start the development server

The development server compiles your code and serves it in the browser. It also watches for file changes and refreshes automatically.

  • Start the dev server by running:
npm run dev
  • Open your browser and navigate to http://localhost:3000.
  • You should see the default Next.js welcome page with a dark background and the Next.js logo.

Page not loading?

Make sure you ran npm run dev from inside the feedback-board folder, not from your Desktop.

Check your terminal for error messages. If port 3000 is already in use, stop the other process first.

Help me get my Next.js dev server running

Clean up the default page

The scaffolded project comes with a demo page full of links and images. You will replace it with a simple heading so you have a clean starting point.

  • Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing VS Code, and pressing Enter.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the feedback-board folder, and click Open.
  • In the Explorer sidebar on the left, expand the app folder and click on page.tsx to open it.
  • Select all the existing code in page.tsx by pressing Cmd+A (macOS) or Ctrl+A (Windows).
  • Delete all the selected code, then paste in the following:
export default function Home() {
  return (
    <div>
      <h1>Welcome</h1>
    </div>
  );
}

What does this code do?

This file exports a React component called Home. Next.js automatically renders this component when a user visits the root URL (/).

The <h1>Welcome</h1> inside the return statement is JSX. It looks like HTML but lives inside a JavaScript function.

  • Save page.tsx by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Switch to your browser. The page should automatically refresh, showing just the word "Welcome" in the top left.

Still seeing the default page?

Make sure you saved the file. Check for a dot on the page.tsx tab in VS Code, which indicates unsaved changes.

Confirm you edited app/page.tsx and not a different file. The file should be directly inside the app folder.

Help me figure out why my page.tsx changes are not showing

✔️ Awesome, I've got everything!

Great. Double check you have saved app/page.tsx and your browser shows just "Welcome" on the page.

ⓧ I'd like to double check the full code

Here is the complete app/page.tsx at the end of this step:

export default function Home() {
  return (
    <div>
      <h1>Welcome</h1>
    </div>
  );
}

Your project is scaffolded and running. Next up, you will build a shared layout with navigation so users can move between pages.

Build the Layout and Navigation

Your project is running and you have a clean starting page. Now it's time to give your app structure.

Every page on your feedback board needs a navigation bar so users can switch between submitting feedback and viewing it. In Next.js, a layout file wraps every page with shared UI like headers and footers. You will edit the root layout to add navigation links and create a second page for the app.

In this step, get ready to:
  • Add a navigation header to the root layout with links to both pages.
  • Create a new feedback page route using file-based routing.
  • Test that navigation switches page content without reloading the whole page.
Edit the root layout to add navigation

The root layout (app/layout.tsx) wraps every page in your app. Right now it only renders the page content with no navigation. You will add a Link component from Next.js that handles navigation without full page reloads.

  • In VS Code, open app/layout.tsx from the Explorer sidebar.
  • Replace the entire file content with this code:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
import "./globals.css";

// Configure the Geist font family for the app
const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

// Set the page title and description for the browser tab
export const metadata: Metadata = {
  title: "Feedback Board",
  description: "A full-stack feedback board built with Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {/* Navigation bar with links to each page */}
        <nav className="border-b border-gray-200 bg-white px-6 py-4">
          <div className="mx-auto flex max-w-4xl items-center gap-6">
            <Link href="/" className="text-lg font-semibold">
              Feedback Board
            </Link>
            <Link
              href="/"
              className="text-gray-600 hover:text-gray-900"
            >
              Submit
            </Link>
            <Link
              href="/feedback"
              className="text-gray-600 hover:text-gray-900"
            >
              View Feedback
            </Link>
          </div>
        </nav>
        {/* Main content area where page content renders */}
        <main className="mx-auto max-w-4xl px-6 py-8">{children}</main>
      </body>
    </html>
  );
}

What does this code do?

The key change from the default layout is the <nav> element with three Link components. The Link component from Next.js navigates between pages on the client side, so the browser does not do a full refresh.

The metadata export sets the browser tab title to "Feedback Board" instead of the default "Create Next App". The {children} prop inside <main> is where each page's content gets rendered.

  • Save app/layout.tsx.
  • Switch to your browser at http://localhost:3000. You should see a white navigation bar at the top with "Feedback Board", "Submit", and "View Feedback" links.

Don't see the navigation bar?

  • Make sure you saved the file. Unsaved changes will not appear in the browser.
  • Check that import Link from "next/link"; appears at the top of the file. Without this import, the Link component will cause an error.
  • Look at your terminal for red error text. A missing closing tag or typo in the JSX will prevent the page from rendering.

Help me fix my layout.tsx file so the navigation bar appears.

Create the feedback page route

Next.js uses file-based routing. Each folder inside app/ becomes a URL segment, and a page.tsx file inside that folder makes the route accessible. To create a /feedback page, you need to create a feedback folder with its own page.tsx.

  • In the VS Code Explorer sidebar, right-click on the app folder.
  • Click New Folder and name it feedback.
  • Right-click on the new feedback folder and click New File.
  • Name the file page.tsx.
  • Add this code to app/feedback/page.tsx:
export default function FeedbackPage() {
  return (
    <div>
      <h1>Feedback</h1>
    </div>
  );
}

What does this code do?

This is a minimal page component that just renders a heading. Because the file lives at app/feedback/page.tsx, Next.js automatically maps it to the /feedback URL. No router configuration needed.

  • Save app/feedback/page.tsx.
  • In your browser, click the View Feedback link in the navigation bar. You should see the "Feedback" heading appear below the nav bar.

Seeing a 404 page?

  • Confirm the file is named exactly page.tsx (not Page.tsx or feedback.tsx). Next.js only recognizes the lowercase page.tsx convention.
  • Make sure the file is inside app/feedback/ and not directly inside app/. The folder structure determines the URL.

Why is my /feedback page showing a 404 error?

Test navigation between both pages
  • Click the Submit link in the navigation bar to go back to the home page.
  • Click View Feedback again to switch to the feedback page.

Notice that the navigation bar stays in place the entire time. The page content below it swaps out without a full browser reload. This is the layout in action.

Why does the layout persist?

The root layout (app/layout.tsx) renders once and stays mounted while you navigate. Only the {children} portion updates with the new page's content. This gives you instant page transitions without rebuilding the navigation bar each time.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete app/layout.tsx:

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Feedback Board",
  description: "A full-stack feedback board built with Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <nav className="border-b border-gray-200 bg-white px-6 py-4">
          <div className="mx-auto flex max-w-4xl items-center gap-6">
            <Link href="/" className="text-lg font-semibold">
              Feedback Board
            </Link>
            <Link
              href="/"
              className="text-gray-600 hover:text-gray-900"
            >
              Submit
            </Link>
            <Link
              href="/feedback"
              className="text-gray-600 hover:text-gray-900"
            >
              View Feedback
            </Link>
          </div>
        </nav>
        <main className="mx-auto max-w-4xl px-6 py-8">{children}</main>
      </body>
    </html>
  );
}

Here is the complete app/feedback/page.tsx:

export default function FeedbackPage() {
  return (
    <div>
      <h1>Feedback</h1>
    </div>
  );
}

Your app now has structure. Two pages share a common layout, and users can move between them instantly. Next up, you will build the server-side logic that stores and retrieves feedback data.

Create the Server Action and Data Store

Your layout and navigation are working. Clicking between pages feels instant because the Link component handles routing on the client side.

Now you need the server-side logic that makes your feedback board actually work. You need somewhere to store feedback submissions and a function that processes form data when a user hits submit. In this step, you will create both pieces.

In this step, get ready to:
  • Create an in-memory data store with helper functions for reading and writing feedback.
  • Create a Server Action that processes form submissions and refreshes the feedback page.
Create the data store

Every app needs somewhere to keep its data. For this project, you will store feedback in a simple array that lives in server memory. This keeps the project zero-cost and avoids setting up a database.

Why an in-memory array instead of a database?

An in-memory store lets you focus on learning Next.js patterns without managing database connections, migrations, or credentials. The trade-off is that your data resets every time the dev server restarts.

  • In the VS Code Explorer sidebar, right-click the app folder and select New Folder.
  • Name the folder lib.
  • Right-click the new lib folder and select New File.
  • Name the file data.ts.
  • Define the shape of a feedback item and an empty store by pasting this code into data.ts:
export interface FeedbackItem {
  id: string;
  name: string;
  message: string;
  createdAt: string;
}

const feedbackStore: FeedbackItem[] = [];

What does this code do?

The FeedbackItem interface tells TypeScript exactly what properties each feedback entry has. This gives you autocomplete in VS Code and catches typos early.

The feedbackStore array is your data storage. It starts empty and lives in server memory for as long as the dev server is running.

  • Add the helper functions below the feedbackStore line in the same file:
export function getFeedback(): FeedbackItem[] {
  // Return a reversed copy so newest feedback appears first
  return [...feedbackStore].reverse();
}

export function addFeedback(name: string, message: string): void {
  // Push a new feedback item with a unique ID and timestamp
  feedbackStore.push({
    id: crypto.randomUUID(),
    name,
    message,
    createdAt: new Date().toISOString(),
  });
}

What do these functions do?

getFeedback() returns a reversed copy of the array so the newest feedback shows up first. The spread operator [...feedbackStore] creates a copy so you don't mutate the original.

addFeedback() pushes a new item into the store with a random ID (from crypto.randomUUID()) and the current timestamp. These are generated automatically so the form only needs to collect name and message.

  • Save data.ts.
  • Check that VS Code shows no red underlines in the file. If the file is error-free, TypeScript is happy with your types and exports.

Seeing red underlines in data.ts?

  • Make sure the file is saved inside app/lib/data.ts, not directly in the app folder.
  • Check that you used .ts as the file extension, not .tsx. This file has no JSX so it does not need the .tsx extension.
  • Confirm there are no typos in the property names inside the interface. They must match exactly: id, name, message, createdAt.

Why is my data.ts file showing TypeScript errors?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

export interface FeedbackItem {
  id: string;
  name: string;
  message: string;
  createdAt: string;
}

const feedbackStore: FeedbackItem[] = [];

export function getFeedback(): FeedbackItem[] {
  return [...feedbackStore].reverse();
}

export function addFeedback(name: string, message: string): void {
  feedbackStore.push({
    id: crypto.randomUUID(),
    name,
    message,
    createdAt: new Date().toISOString(),
  });
}
Create the server action

Your data store is ready to accept feedback. Now you need a function that runs on the server when a user submits the form. In Next.js, these are called Server Actions.

What is a Server Action?

A Server Action is an async function that runs on the server in response to a user interaction (like submitting a form). You mark a file with 'use server' at the top, and Next.js handles the network call for you.

This means you do not need to build a separate API server or write fetch requests. The form sends its data directly to your server function.

  • Right-click the lib folder in the Explorer sidebar and select New File.
  • Name the file actions.ts.
  • Paste the following code into actions.ts:
"use server";

import { revalidatePath } from "next/cache";
import { addFeedback } from "./data";

export async function submitFeedback(formData: FormData): Promise<void> {
  // Extract the name and message fields from the form
  const name = formData.get("name") as string;
  const message = formData.get("message") as string;

  // Validate that neither field is empty
  if (!name || !message) {
    throw new Error("Name and message are required");
  }

  // Save the feedback to the store
  addFeedback(name, message);
  // Tell Next.js to refresh the feedback page so it shows the new data
  revalidatePath("/feedback");
}

What does this code do?

  • "use server" at the top marks every exported function in this file as a Server Action. Next.js will only run these functions on the server, never in the browser.
  • formData.get("name") extracts the value the user typed into the name field. The browser packs form inputs into a FormData object automatically.
  • revalidatePath("/feedback") tells Next.js that the /feedback page has stale data and needs to re-render with the latest feedback.
  • Save actions.ts.
  • Check that VS Code shows no red underlines in the file. The import from ./data should resolve without errors because your data.ts file is in the same folder.

Seeing an error on the import line?

  • Make sure actions.ts is inside the same app/lib folder as data.ts. The relative import ./data only works if both files are in the same directory.
  • Confirm the file is named data.ts (not data.js or Data.ts). File names are case-sensitive.

Help me fix an import error in my actions.ts file.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

"use server";

import { revalidatePath } from "next/cache";
import { addFeedback } from "./data";

export async function submitFeedback(formData: FormData): Promise<void> {
  const name = formData.get("name") as string;
  const message = formData.get("message") as string;

  if (!name || !message) {
    throw new Error("Name and message are required");
  }

  addFeedback(name, message);
  revalidatePath("/feedback");
}

Your server-side logic is in place. Next up, you will build the form that users interact with and wire it to the submitFeedback action so that submitting feedback actually works.

Build the Feedback Form

Your server action and data store are ready to receive feedback. Now you need a way for users to actually submit it.

In this step, you'll build a form component that collects a name and message from the user. The form sends data directly to your Server Action without any API routes or fetch calls.

In this step, get ready to:
  • Create the FeedbackForm Client Component.
  • Wire the form into the home page.
  • Test that form submission works end to end.
Create the FeedbackForm Client Component

In Next.js, components that use browser features like refs or interactive state need the 'use client' directive at the top of the file.

This tells Next.js to render the component in the browser instead of on the server. Your form needs this because it uses a ref to reset the input fields after submission.

  • In the VS Code Explorer sidebar, right-click the app folder and select New Folder.
  • Name the folder components.
  • Right-click the new components folder and select New File.
  • Name the file FeedbackForm.tsx.
  • Paste the following code into FeedbackForm.tsx:
"use client";

import { submitFeedback } from "../lib/actions";
import { useRef } from "react";

export default function FeedbackForm() {
  // Create a ref to access the form element directly
  const formRef = useRef<HTMLFormElement>(null);

  // Wrapper that calls the server action then resets the form
  async function handleSubmit(formData: FormData) {
    await submitFeedback(formData);
    formRef.current?.reset();
  }

  return (
    <form ref={formRef} action={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium text-gray-700">
          Name
        </label>
        <input
          type="text"
          id="name"
          name="name"
          required
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
          placeholder="Your name"
        />
      </div>
      <div>
        <label htmlFor="message" className="block text-sm font-medium text-gray-700">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          required
          rows={4}
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
          placeholder="Your feedback..."
        />
      </div>
      <button
        type="submit"
        className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        Submit Feedback
      </button>
    </form>
  );
}

What does this code do?

"use client" at the top marks this as a Client Component. Without it, Next.js would try to run this on the server where browser APIs like useRef don't exist.

formRef holds a reference to the <form> element. After the server action completes, formRef.current?.reset() clears all the input fields so the user can submit again.

The action attribute on the form accepts a function. Next.js automatically collects the form fields into a FormData object and passes it to handleSubmit when the user clicks the button.

  • Save FeedbackForm.tsx.

Seeing a red underline on the import?

If VS Code shows an error on import { submitFeedback } from "../lib/actions", make sure your file is saved inside app/components/ (not directly inside app/). The relative path ../lib/ only resolves correctly from the components subfolder.

help me fix the import error in FeedbackForm.tsx

Wire the form into the home page

The form component is ready, but nobody can see it yet. You need to import it into the home page and render it alongside a heading and description.

  • Open app/page.tsx in VS Code.
  • Replace the entire file content with the following code:
import FeedbackForm from "./components/FeedbackForm";

export default function Home() {
  return (
    <div>
      <h1 className="text-3xl font-bold">Submit Feedback</h1>
      <p className="mt-2 text-gray-600">
        Share your thoughts, report bugs, or request features.
      </p>
      <div className="mt-6">
        {/* Render the Client Component form */}
        <FeedbackForm />
      </div>
    </div>
  );
}

What's happening here?

This page is a Server Component (the default in Next.js). It imports and renders the FeedbackForm Client Component inside it.

This pattern is common in Next.js. The page renders on the server for fast initial load, while the interactive form renders in the browser where it can handle user input.

  • Save app/page.tsx.
  • Switch to your browser at http://localhost:3000. You should see the heading "Submit Feedback", a description paragraph, and a form with Name and Message fields plus a blue Submit Feedback button.

Don't see the form?

Make sure you saved both FeedbackForm.tsx and page.tsx. The dev server auto-refreshes on save, but unsaved files won't trigger it.

Check that FeedbackForm.tsx is inside app/components/. The import path ./components/FeedbackForm in page.tsx expects that exact location.

help me debug why my FeedbackForm isn't showing up

Test the form submission

The form is visible, but you need to confirm it actually sends data to your server action without crashing.

  • Click into the Name field and type any name.
  • Click into the Message field and type a short message.
  • Click the Submit Feedback button.

After you click submit, both fields should clear automatically. This means the server action ran successfully and the form reset itself.

  • Check your terminal where the dev server is running. There should be no red error messages.
  • Submit the form 2-3 more times with different names and messages. Each time the fields should clear without errors.

Form doesn't clear after submitting?

Make sure handleSubmit in your FeedbackForm.tsx uses await before submitFeedback(formData). Without await, the reset runs before the action finishes.

Also verify that formRef is attached to the <form> element with ref={formRef}. If the ref isn't connected, formRef.current will be null and reset won't fire.

help me fix my form reset

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete app/components/FeedbackForm.tsx:

"use client";

import { submitFeedback } from "../lib/actions";
import { useRef } from "react";

export default function FeedbackForm() {
  const formRef = useRef<HTMLFormElement>(null);

  async function handleSubmit(formData: FormData) {
    await submitFeedback(formData);
    formRef.current?.reset();
  }

  return (
    <form ref={formRef} action={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium text-gray-700">
          Name
        </label>
        <input
          type="text"
          id="name"
          name="name"
          required
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
          placeholder="Your name"
        />
      </div>
      <div>
        <label htmlFor="message" className="block text-sm font-medium text-gray-700">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          required
          rows={4}
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
          placeholder="Your feedback..."
        />
      </div>
      <button
        type="submit"
        className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        Submit Feedback
      </button>
    </form>
  );
}

And the complete app/page.tsx:

import FeedbackForm from "./components/FeedbackForm";

export default function Home() {
  return (
    <div>
      <h1 className="text-3xl font-bold">Submit Feedback</h1>
      <p className="mt-2 text-gray-600">
        Share your thoughts, report bugs, or request features.
      </p>
      <div className="mt-6">
        <FeedbackForm />
      </div>
    </div>
  );
}

Your form is working and sending data to the server. Next up, you'll build the feedback display page so you can actually see the submissions you've been making.

Display Feedback on the Feedback Page

Your form is submitting data to the server successfully. The next question is: how do you display that data back to users on a different page?

In this step, you'll update the feedback page to read data directly from the server and render it as styled cards. Because Next.js pages are Server Components by default, you can call server-side functions without any extra API layer or useEffect calls.

In this step, get ready to:
  • Update the feedback page to fetch and display all submitted feedback.
  • Test the full-stack loop by submitting and viewing feedback.
  • Confirm the styled feedback cards render correctly.
Update the feedback page to display submitted feedback

The feedback page currently shows a placeholder heading. You need it to pull data from the server and display each feedback item.

Why no useEffect or fetch?

In a traditional React app, you would use useEffect to fetch data from an API endpoint after the page loads. Next.js Server Components run on the server before the page is sent to the browser.

This means you can call your data functions directly in the component body. No API route needed, no loading states to manage, no extra network request.

  • In VS Code, open app/feedback/page.tsx.
  • Replace the entire file contents with the following code:
import { getFeedback } from "../lib/data";

export default function FeedbackPage() {
  // Call the data store directly — no useEffect or fetch needed
  const feedbackItems = getFeedback();

  return (
    <div>
      <h1 className="text-3xl font-bold">All Feedback</h1>
      <p className="mt-2 text-gray-600">
        {feedbackItems.length} item{feedbackItems.length !== 1 ? "s" : ""}{" "}
        submitted
      </p>
      <div className="mt-6 space-y-4">
        {feedbackItems.length === 0 ? (
          <p className="text-gray-500">
            No feedback yet. Be the first to submit!
          </p>
        ) : (
          feedbackItems.map((item) => (
            <div
              key={item.id}
              className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
            >
              <div className="flex items-center justify-between">
                <span className="font-medium">{item.name}</span>
                <span className="text-sm text-gray-500">
                  {new Date(item.createdAt).toLocaleDateString()}
                </span>
              </div>
              <p className="mt-2 text-gray-700">{item.message}</p>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

What does this code do?

  • getFeedback() is called directly in the component body. This works because the page is a Server Component that runs on the server before sending HTML to the browser.
  • The counter paragraph shows how many items have been submitted, using a ternary to handle singular vs. plural.
  • When the array is empty, a placeholder message appears. When items exist, each one renders as a card showing the submitter's name, date, and message.
  • key={item.id} gives React a stable identifier for each card so it can efficiently update the list.
  • Save app/feedback/page.tsx.
  • Switch to your browser and click View Feedback in the nav. You should see the heading "All Feedback" and the message "No feedback yet. Be the first to submit!".

Seeing an error instead of the page?

  • Check that the import path is ../lib/data (two dots, going up one folder from app/feedback/ to app/, then into lib/).
  • Make sure there is no 'use client' directive at the top of this file. It must remain a Server Component to call getFeedback() directly.
  • Confirm that app/lib/data.ts exports the getFeedback function (check for typos in the function name).

Help me debug my feedback page import error.

Test the full-stack loop

The feedback page works, but it's empty. Time to prove the full loop: submit feedback on the home page, then see it appear on the feedback page.

  • Click Submit in the nav to return to the home page.
  • Enter a name (e.g. Alice) in the Name field.
  • Enter a message (e.g. Love the new dashboard!) in the Message field.
  • Click Submit Feedback.
  • Submit a second item with a different name and message so you can see multiple cards.
  • Click View Feedback in the nav.

You should see both feedback items displayed as cards, with the most recent one at the top. Each card shows the name, date, and message.

Why does the newest item appear first?

The getFeedback() function in data.ts calls .reverse() on the array before returning it. This gives you newest-first ordering without needing a database query.

Feedback page still shows "No feedback yet"?

  • Check your terminal for errors. If the Server Action threw an error (missing name or message), the data never reached the store.
  • Make sure you clicked Submit Feedback and the form cleared. If the form did not clear, the submission may have failed silently.
  • Remember that the in-memory store resets if you restart the dev server. If you stopped and restarted npm run dev, you'll need to submit feedback again.

Help me figure out why my feedback isn't showing up.

Confirm the styled feedback cards

The feedback cards use Tailwind CSS utility classes for a clean, readable layout. Take a moment to confirm the visual result matches what you expect.

  • On the feedback page, verify each card has a white background with a subtle border and shadow.
  • Confirm the submitter's name appears on the left in bold, and the date appears on the right in gray.
  • Confirm the message text appears below the name/date row.

How does the card styling work?

  • rounded-lg border border-gray-200 bg-white p-4 shadow-sm creates the card container with rounded corners, a light border, white background, padding, and a subtle shadow.
  • flex items-center justify-between on the inner div pushes the name to the left and the date to the right.
  • space-y-4 on the outer wrapper adds consistent vertical spacing between cards.

✔️ Awesome, I've got everything!

Great. Double check you've saved app/feedback/page.tsx.

ⓧ I'd like to double check the full code

Here is the complete app/feedback/page.tsx file:

import { getFeedback } from "../lib/data";

export default function FeedbackPage() {
  const feedbackItems = getFeedback();

  return (
    <div>
      <h1 className="text-3xl font-bold">All Feedback</h1>
      <p className="mt-2 text-gray-600">
        {feedbackItems.length} item{feedbackItems.length !== 1 ? "s" : ""}{" "}
        submitted
      </p>
      <div className="mt-6 space-y-4">
        {feedbackItems.length === 0 ? (
          <p className="text-gray-500">
            No feedback yet. Be the first to submit!
          </p>
        ) : (
          feedbackItems.map((item) => (
            <div
              key={item.id}
              className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
            >
              <div className="flex items-center justify-between">
                <span className="font-medium">{item.name}</span>
                <span className="text-sm text-gray-500">
                  {new Date(item.createdAt).toLocaleDateString()}
                </span>
              </div>
              <p className="mt-2 text-gray-700">{item.message}</p>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

You've just completed the full-stack loop. Data flows from the form, through a Server Action, into the data store, and back out to a Server Component that renders it. Next up, you'll tackle the Secret Mission and add category filtering to take this board to the next level.

Secret mission

Add Category Filtering

Your feedback board works, but all feedback arrives in one undifferentiated list. Real feedback tools let users categorize submissions and filter by type. Can you add a category system that lets users tag their feedback as Bug, Feature, or General, then filter the feedback page by category using URL search params?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Your Next.js project folder on your Desktop.
  • The development server process (currently running in your terminal).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your feedback board.

  • Your development server stays running at http://localhost:3000.
  • Your project folder remains on your Desktop, ready to edit.

Pause - I'll come back to this later

Stop the development server to free up your terminal. Your files stay on disk so you can pick up where you left off.

  • In your terminal, press Ctrl+C to stop the development server.
  • When you're ready to resume, open your terminal, navigate back to your project folder, and run:
npm run dev

Delete - I don't want to use this again

Remove all project files from your machine to start fresh.

  • In your terminal, press Ctrl+C to stop the development server if it is still running.
  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your-project-folder-name"]]

On Windows, use this command instead:

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your-project-folder-name"]]
  • Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows). Your project folder should no longer appear in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built a full-stack feedback board using Next.js, handling both the UI and server logic in a single project.

You've learned how to:

  • Built a Next.js App Router project with file-based routing, a shared layout, and client-side navigation using the Link component.
  • Wrote a Server Action to handle form submissions without needing a separate backend server.
  • Displayed server-side data directly in a Server Component without useEffect or fetch calls, completing the full-stack loop.
  • Secret Mission: Added category-based filtering using URL search params and the searchParams prop.

Ready to quiz yourself?