Vercel Logo

Build the Summary Endpoint

Raw data is great when you need specifics. But sometimes you want the big picture. How many people left feedback? What's the average rating? Which course is struggling?

Nobody wants to curl 200 entries and do math in their head. Let's build an endpoint that does the math for us.

Outcome

Create a GET /api/feedback/summary route that returns aggregate statistics.

Fast Track

  1. Open app/api/feedback/summary/route.ts (stub provided in starter)
  2. Calculate totals, averages, and rating distribution
  3. Group stats by course

Hands-on exercise

Open app/api/feedback/summary/route.ts. The starter has this file with a stub handler. This endpoint supports one optional query parameter: courseSlug, which filters the data before aggregating.

The response shape should look like this:

{
  "totalEntries": 10,
  "averageRating": 4.2,
  "ratingDistribution": {
    "1": 0,
    "2": 1,
    "3": 1,
    "4": 3,
    "5": 5
  },
  "courses": [
    {
      "courseSlug": "knife-skills",
      "totalEntries": 5,
      "averageRating": 4.4
    }
  ]
}

A few implementation notes:

Rating distribution is an object where keys are ratings 1 through 5 and values are counts. Initialize all five keys to zero before counting, so the response always includes every rating level, even if nobody gave a 1.

Average rating should be rounded to one decimal place. Math.round(value * 10) / 10 handles that cleanly.

Per-course breakdown groups entries by courseSlug and computes totalEntries and averageRating for each. A Map works well here since you're building up state while iterating.

Empty state: If there's no feedback (or the courseSlug filter matches nothing), return zeros across the board with an empty courses array. Don't return a 404. An empty summary is a valid summary.

Route ordering

Next.js matches static routes before dynamic ones. /api/feedback/summary won't conflict with /api/feedback/[id] because summary is a static segment and [id] is dynamic.

Division by zero on empty feedback

If the feedback array is empty, dividing the sum of ratings by feedback.length gives you NaN. Your API would return "averageRating": null in JSON, which is confusing for any consumer. Handle the empty case explicitly and return 0 for the average before you ever reach the division.

If averageRating shows null in the response

You've hit the division-by-zero case. Check that you return early with zeros when the feedback array is empty, before computing the average. The empty check should come right after filtering.

If ratingDistribution is missing keys

Initialize all five rating keys (1 through 5) to zero before iterating. If you build the distribution by only counting what exists in the data, ratings with zero entries won't appear in the response. Agents expect a predictable shape.

Try It

Full summary:

curl http://localhost:3000/api/feedback/summary
{
  "totalEntries": 10,
  "averageRating": 4.2,
  "ratingDistribution": {
    "1": 0,
    "2": 1,
    "3": 1,
    "4": 3,
    "5": 5
  },
  "courses": [
    {
      "courseSlug": "knife-skills",
      "totalEntries": 5,
      "averageRating": 4.4
    },
    {
      "courseSlug": "bread-baking",
      "totalEntries": 4,
      "averageRating": 4.0
    },
    {
      "courseSlug": "pasta-from-scratch",
      "totalEntries": 1,
      "averageRating": 4.0
    }
  ]
}

Summary for one course:

curl "http://localhost:3000/api/feedback/summary?courseSlug=knife-skills"

Should return stats for just the knife-skills entries.

Summary for a nonexistent course:

curl "http://localhost:3000/api/feedback/summary?courseSlug=underwater-basket-weaving"
{
  "totalEntries": 0,
  "averageRating": 0,
  "ratingDistribution": { "1": 0, "2": 0, "3": 0, "4": 0, "5": 0 },
  "courses": []
}

No 404. Zeros are informative.

With this endpoint, nobody has to eyeball 200 entries to figure out which course needs attention. One request, one response, all the numbers that matter.

Commit

git add -A && git commit -m "feat(api): add summary endpoint with aggregate stats"

Done-When

  • GET /api/feedback/summary returns totalEntries, averageRating, ratingDistribution, and courses
  • averageRating is rounded to one decimal place
  • ratingDistribution always includes keys 1 through 5
  • courseSlug query param filters before aggregating
  • Empty results return zeros, not a 404

Solution

app/api/feedback/summary/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAllFeedback } from "@/lib/data";
 
export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl;
  const courseSlug = searchParams.get("courseSlug");
 
  let feedback = await getAllFeedback();
 
  if (courseSlug) {
    feedback = feedback.filter((fb) => fb.courseSlug === courseSlug);
  }
 
  if (feedback.length === 0) {
    return NextResponse.json({
      totalEntries: 0,
      averageRating: 0,
      ratingDistribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
      courses: [],
    });
  }
 
  const avgRating =
    feedback.reduce((sum, fb) => sum + fb.rating, 0) / feedback.length;
 
  const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 } as Record<number, number>;
  for (const fb of feedback) {
    distribution[fb.rating]++;
  }
 
  const courseMap = new Map<string, { count: number; sum: number }>();
  for (const fb of feedback) {
    const existing = courseMap.get(fb.courseSlug) ?? { count: 0, sum: 0 };
    existing.count++;
    existing.sum += fb.rating;
    courseMap.set(fb.courseSlug, existing);
  }
 
  const courses = [...courseMap.entries()].map(([slug, data]) => ({
    courseSlug: slug,
    totalEntries: data.count,
    averageRating: Math.round((data.sum / data.count) * 10) / 10,
  }));
 
  return NextResponse.json({
    totalEntries: feedback.length,
    averageRating: Math.round(avgRating * 10) / 10,
    ratingDistribution: distribution,
    courses,
  });
}