Vercel Logo

Build the Feedback Endpoint

Every restaurant has a comment card. Some wedge them between the salt shaker and the menu. Some follow up with an email. But the card itself is useless without two things: a way to read what people wrote, and a way to submit a new one.

That's what we're building. Two handlers on a single route: GET to read, POST to write.

Outcome

Build a /api/feedback route that returns all feedback entries on GET and creates new ones on POST.

Fast Track

  1. Open app/api/feedback/route.ts (stub provided in starter)
  2. Implement the GET handler to return all feedback as JSON
  3. Implement the POST handler to validate the body and add the entry

Hands-on exercise

Open app/api/feedback/route.ts. The starter has this file with stub handlers. In Next.js App Router, the file path determines the URL. A file at app/api/feedback/route.ts handles requests to /api/feedback.

The GET handler should:

  1. Call getAllFeedback() from your data utility
  2. Return the results as JSON with NextResponse.json()

That's it for now. No filtering yet. We'll add that in the next lesson.

The POST handler is where it gets interesting. We need to:

  1. Parse the JSON body from the request
  2. Validate that all required fields are present: courseSlug, lessonSlug, rating, comment, author
  3. Validate that rating is a number between 1 and 5
  4. Call addFeedback() with the validated data
  5. Return the new entry with a 201 status

For validation errors, return a 400 status with a JSON body containing an error field. Be specific about what went wrong. Vague error messages like "Bad request" are unhelpful for humans and even worse for agents (we'll come back to this point in Section 2).

app/api/feedback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAllFeedback, addFeedback } from "@/lib/data";

Start with those imports, then implement the two handlers.

Why validate on the server?

We don't have a frontend form in this course, but clients (including agents) will POST to this endpoint. Validation at the boundary is the only validation that matters.

Try It

Start the dev server and test both handlers with curl.

List all feedback:

curl http://localhost:3000/api/feedback

You should see all 10 seed entries. The first one will look like this:

[
  {
    "id": "fb-001",
    "courseSlug": "knife-skills",
    "lessonSlug": "the-claw-grip",
    "rating": 5,
    "comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
    "author": "Priya Sharma",
    "createdAt": "2026-03-01T10:30:00Z"
  },
  ...
]

Submit new feedback:

curl -X POST http://localhost:3000/api/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "courseSlug": "bread-baking",
    "lessonSlug": "scoring-dough",
    "rating": 5,
    "comment": "The lame technique demo was incredibly helpful.",
    "author": "Alex Turner"
  }'

You should get back the new entry with a generated id and createdAt:

{
  "id": "fb-011",
  "courseSlug": "bread-baking",
  "lessonSlug": "scoring-dough",
  "rating": 5,
  "comment": "The lame technique demo was incredibly helpful.",
  "author": "Alex Turner",
  "createdAt": "2026-03-06T12:00:00Z"
}

Missing fields test:

curl -X POST http://localhost:3000/api/feedback \
  -H "Content-Type: application/json" \
  -d '{}'
{
  "error": "Missing required fields: courseSlug, lessonSlug, rating, comment, author"
}

Bad rating test:

curl -X POST http://localhost:3000/api/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "courseSlug": "bread-baking",
    "lessonSlug": "scoring-dough",
    "rating": 11,
    "comment": "Off the charts",
    "author": "Alex Turner"
  }'
{
  "error": "Rating must be a number between 1 and 5"
}
POST modifies your seed data

Every successful POST appends to data/feedback.json. If your test data gets messy, reset it with git checkout data/feedback.json.

If POST returns an empty body or 500

Check that you're including -H "Content-Type: application/json" in your curl command. Without it, request.json() can't parse the body and throws an unhandled error. If you see SyntaxError: Unexpected token in the terminal, that's the cause.

If GET returns an empty array

Your seed data file might have been overwritten by a bad POST. Open data/feedback.json and check that it still has the original 10 entries. If it's empty or malformed, copy it fresh from the starter repo.

Two handlers, one route. The comment card has a box to drop it in and a stack to read from. Next we'll add filtering so you're not reading every card in the pile every time.

Commit

git add -A && git commit -m "feat(api): add GET and POST handlers for /api/feedback"

Done-When

  • GET /api/feedback returns all entries from the JSON file
  • POST /api/feedback creates a new entry and returns it with status 201
  • Missing fields return a 400 with a descriptive error message
  • Invalid rating returns a 400 with a descriptive error message

Solution

app/api/feedback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAllFeedback, addFeedback } from "@/lib/data";
 
export async function GET(request: NextRequest) {
  const feedback = await getAllFeedback();
  return NextResponse.json(feedback);
}
 
export async function POST(request: NextRequest) {
  const body = await request.json();
 
  const { courseSlug, lessonSlug, rating, comment, author } = body;
 
  if (!courseSlug || !lessonSlug || rating == null || !comment || !author) {
    return NextResponse.json(
      { error: "Missing required fields: courseSlug, lessonSlug, rating, comment, author" },
      { status: 400 }
    );
  }
 
  if (typeof rating !== "number" || rating < 1 || rating > 5) {
    return NextResponse.json(
      { error: "Rating must be a number between 1 and 5" },
      { status: 400 }
    );
  }
 
  const entry = await addFeedback({ courseSlug, lessonSlug, rating, comment, author });
  return NextResponse.json(entry, { status: 201 });
}