---
title: "Filtering and Details"
description: "Extend the feedback API with query parameter filtering on the list endpoint and a dynamic route segment for fetching individual entries by ID."
canonical_url: "https://vercel.com/academy/agent-friendly-apis/filtering-and-details"
md_url: "https://vercel.com/academy/agent-friendly-apis/filtering-and-details.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-08-26T21:03:36.420Z"
content_type: "lesson"
course: "agent-friendly-apis"
course_title: "Agent-Friendly APIs"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Filtering and Details

# Add Filtering and Details

When someone asks what students are saying about the knife skills course, the API should return that course's feedback instead of the full dataset.

You'll add query parameters to narrow the list and a route for fetching one entry by ID.

## Outcome

Add query param filtering to `GET /api/feedback` and create a `GET /api/feedback/:id` route.

## Fast Track

1. Add `courseSlug`, `lessonSlug`, and `minRating` query param support to the GET handler
2. Implement the GET handler in `app/api/feedback/[id]/route.ts`
3. Return 404 for unknown IDs

## Hands-on exercise

**Part 1: Query parameters**

Update the existing `GET` handler in `app/api/feedback/route.ts` to support three optional query parameters:

- `courseSlug`: filter entries where `courseSlug` matches exactly
- `lessonSlug`: filter entries where `lessonSlug` matches exactly
- `minRating`: filter entries where `rating` is greater than or equal to the value

Pull these from `request.nextUrl.searchParams`. Each filter is optional. If none are provided, return everything (the current behavior). If multiple are provided, apply them all.

```ts
const { searchParams } = request.nextUrl;
const courseSlug = searchParams.get("courseSlug");
```

**Part 2: Single feedback by ID**

Open `app/api/feedback/[id]/route.ts`. The starter has this file with a stub handler. The brackets in `[id]` make this a dynamic segment. Next.js passes the value through the `params` prop.

In Next.js 16, `params` is a Promise. You need to await it:

```ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  // ...
}
```

Look up the feedback using `getFeedbackById`. If it doesn't exist, return a `404` with an error message that includes the requested ID. An agent can use that message to decide what to try next.

\*\*Warning: Async params in Next.js 16\*\*

If you forget to `await params`, TypeScript reports that `id` is a Promise. In Next.js 16, route params are asynchronous.

\*\*Warning: Validate minRating explicitly\*\*

Query parameters arrive as strings. Convert `minRating` with `Number()`, then verify it is an integer from 1 to 5. Relying on implicit coercion or `parseInt()` can silently accept malformed values such as `4stars` or produce `NaN`.

\*\*Warning: If you see 'params.id is a Promise'\*\*

In Next.js 16, `params` is async. If your TypeScript shows an error like "Property 'id' does not exist on type 'Promise'", you forgot to `await params`. The signature needs `{ params }: { params: Promise<{ id: string }> }` and you must `const { id } = await params` before using it.

\*\*Warning: If filters return all entries instead of filtering\*\*

Check that slug comparisons use strict equality (`===`) and `minRating` uses `>=` with the parsed integer. Each `if` block must also update the filtered result.

## Try It

**Filter by course:**

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

Should return only the knife-skills entries (5 in the seed data).

**Filter by minimum rating:**

```bash
curl "http://localhost:3000/api/feedback?minRating=5"
```

Should return only 5-star entries.

**Combine filters:**

```bash
curl "http://localhost:3000/api/feedback?courseSlug=bread-baking&minRating=4"
```

Only bread-baking entries rated 4 or above.

**Fetch a single entry:**

```bash
curl http://localhost:3000/api/feedback/fb-001
```

```json
{
  "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"
}
```

**Fetch a nonexistent entry:**

```bash
curl http://localhost:3000/api/feedback/fb-999
```

```json
{
  "error": "Feedback with id \"fb-999\" not found"
}
```

Clients can now request feedback by course, minimum rating, or ID without reading the full dataset.

## Commit

```bash
git add -A && git commit -m "feat(api): add query param filtering and single-feedback route"
```

## Done-When

- [ ] `GET /api/feedback?courseSlug=knife-skills` returns only knife-skills feedback
- [ ] `GET /api/feedback?minRating=5` returns only 5-star entries
- [ ] Multiple query params combine (AND logic)
- [ ] `GET /api/feedback/fb-001` returns a single entry
- [ ] `GET /api/feedback/fb-999` returns 404 with an error message that includes the ID

## Solution

```ts title="app/api/feedback/route.ts" {5-8,12-23}
import { NextRequest, NextResponse } from "next/server";
import { getAllFeedback, addFeedback } from "@/lib/data";

export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl;
  const courseSlug = searchParams.get("courseSlug");
  const lessonSlug = searchParams.get("lessonSlug");
  const minRating = searchParams.get("minRating");

  let feedback = await getAllFeedback();

  if (courseSlug) {
    feedback = feedback.filter((fb) => fb.courseSlug === courseSlug);
  }

  if (lessonSlug) {
    feedback = feedback.filter((fb) => fb.lessonSlug === lessonSlug);
  }

  if (minRating) {
    const min = Number(minRating);
    if (!Number.isInteger(min) || min < 1 || min > 5) {
      return NextResponse.json(
        { error: "minRating must be an integer between 1 and 5" },
        { status: 400 }
      );
    }
    feedback = feedback.filter((fb) => fb.rating >= min);
  }

  return NextResponse.json(feedback);
}

export async function POST(request: NextRequest) {
  let body;

  try {
    body = await request.json();
  } catch {
    return NextResponse.json(
      { error: "Request body must be valid JSON" },
      { status: 400 }
    );
  }

  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 (!Number.isInteger(rating) || rating < 1 || rating > 5) {
    return NextResponse.json(
      { error: "Rating must be an integer between 1 and 5" },
      { status: 400 }
    );
  }

  const entry = await addFeedback({ courseSlug, lessonSlug, rating, comment, author });
  return NextResponse.json(entry, { status: 201 });
}
```

```ts title="app/api/feedback/[id]/route.ts"
import { NextRequest, NextResponse } from "next/server";
import { getFeedbackById } from "@/lib/data";

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const feedback = await getFeedbackById(id);

  if (!feedback) {
    return NextResponse.json(
      { error: `Feedback with id "${id}" not found` },
      { status: 404 }
    );
  }

  return NextResponse.json(feedback);
}
```


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
