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
- Open
app/api/feedback/summary/route.ts(stub provided in starter) - Calculate totals, averages, and rating distribution
- 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.
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.
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.
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.
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/summaryreturnstotalEntries,averageRating,ratingDistribution, andcoursesaverageRatingis rounded to one decimal placeratingDistributionalways includes keys 1 through 5courseSlugquery param filters before aggregating- Empty results return zeros, not a 404
Solution
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,
});
}Was this helpful?