Introduction: The Need for Filtering Tasks

Welcome back! So far, you have learned how to organize your backend code, validate data, and secure your Task Manager API. Now, let’s make your API even more useful by allowing users to filter tasks based on whether they are completed.

Imagine you have a long list of tasks. Sometimes, you only want to see what’s left to do, and other times, you want to review what you’ve already finished. Filtering tasks by their completion status makes this possible. In this lesson, you will learn how to add this feature to your API.

Service Layer Overview: Filtering Logic

Let’s quickly go through the service functions you'll use to implement filtering.

In your taskService.ts, you have:

export const getAllTasks = (): Task[] => tasks;

// ...other functions

export const filterTaskByStatus = (completed: boolean): Task[] =>
  tasks.filter(t => t.completed === completed);

Here’s what each one does:

  • getAllTasks() returns every task in your in-memory list.
  • filterTaskByStatus(completed) returns only the tasks that match the completed status (true or false).
Using Query Parameters to Filter API Results

To let users choose which tasks they want to see, you will use something called a query parameter. Query parameters are extra pieces of information added to the end of a URL. They help you filter or sort data when making API requests.

For example, if you want to see only completed tasks, you might use a URL like this:

/api/tasks/filter?completed=true

Here, completed is the query parameter, and true is its value. If you want to see only incomplete tasks, you would use:

/api/tasks/filter?completed=false

Your API will read this parameter and use it to decide which tasks to return. This type of filtering is stateless — the client decides what it wants to see (completed=true or false), and the API responds accordingly without needing to track user sessions or state.

Building the Filter Endpoint

Now, let’s build the endpoint that filters tasks by their completion status. Here is the code for the filter endpoint:

// src/app/api/tasks/filter/route.ts
import { NextRequest } from 'next/server';
import { filterTaskByStatus } from '@/lib/services/taskService';
import { createErrorResponse, createSuccessResponse } from '@/lib/responses';

export async function GET(request: NextRequest) {
  const completedParam = request.nextUrl.searchParams.get('completed');
  if (completedParam === null) {
    return createErrorResponse("'completed' query parameter is required", 400);
  }
  if (completedParam !== 'true' && completedParam !== 'false') {
    return createErrorResponse("'completed' must be 'true' or 'false'", 400);
  }
  const completed = completedParam === 'true';
  const tasks = filterTaskByStatus(completed);
  return createSuccessResponse(tasks);
}

Let’s break down what’s happening here:

  1. Reading the Query Parameter
    The code gets the value of the completed query parameter from the request URL:

    const completedParam = request.nextUrl.searchParams.get('completed');

    If the parameter is missing, it returns an error.

  2. Validating the Parameter
    The code checks if the value is either true or false. If not, it returns an error message:

    if (completedParam !== 'true' && completedParam !== 'false') {
      return createErrorResponse("'completed' must be 'true' or 'false'", 400);
    }
  3. Filtering the Tasks
    The code converts the string to a boolean and uses the service function to filter tasks:

    const completed = completedParam === 'true';
    const tasks = filterTaskByStatus(completed);
  4. Returning the Result
    Finally, it returns the filtered list of tasks in a success response.

Example Output:
If you call /api/tasks/filter?completed=true, you might get a response like:

[
  {
    "id": 2,
    "title": "Finish project",
    "content": "Complete the API section",
    "completed": true,
    "dueDate": "2024-06-15T00:00:00.000Z"
  }
]

If you call /api/tasks/filter?completed=false, you’ll get only the tasks that are not completed.

How Filtering Works in the Service Layer

The actual filtering happens in the service layer, using this function:

export const filterTaskByStatus = (completed: boolean): Task[] =>
  tasks.filter(t => t.completed === completed);

Here’s what’s happening:

  • The function takes a boolean value (true or false).
  • It goes through the list of tasks and keeps only those where the completed property matches the value you provided.

For example, if you pass true, you get only completed tasks. If you pass false, you get only incomplete tasks.

Frontend Integration: Filtering with Buttons and Query Params

To help users filter tasks visually, the frontend includes two buttons: one for incomplete tasks, and one for completed ones. These buttons simply update the URL and attach the API key as a query parameter: Here’s the TSX code:

<Link href={`/tasks/filter?completed=false&apiKey=${encodeURIComponent(apiKey)}`}>
  <button disabled={!apiKey}>Show Incomplete Tasks</button>
</Link>
<Link href={`/tasks/filter?completed=true&apiKey=${encodeURIComponent(apiKey)}`}>
  <button disabled={!apiKey} style={{ marginLeft: '0.5rem' }}>
    Show Completed Tasks
  </button>
</Link>

What happens when the button is clicked?

  • The browser navigates to /tasks/filter?completed=true&apiKey=....
  • The /tasks/filter/page.tsx page reads both the completed and apiKey query parameters from the URL.
  • It then makes a request to your backend endpoint /api/tasks/filter?completed=true and sends the API key in the request header.

Let’s look a bit deeper at how the /tasks/filter/page.tsx file handles filtering based on the URL.

This file is responsible for:

  • Reading the completed and apiKey query parameters from the URL
  • Making a GET request to your /api/tasks/filter backend route
  • Displaying either a list of tasks or an error

Here’s a simplified but expanded version of the relevant logic:

export default function FilterPage() {
  const searchParams = useSearchParams();
  const completedParam = searchParams.get('completed');
  const apiKey = searchParams.get('apiKey');

  const [tasks, setTasks] = useState([]);
  const [error, setError] = useState('');

  // Run this logic whenever the 'completed' query param changes
  useEffect(() => {
    if (completedParam !== 'true' && completedParam !== 'false') {
      setError("Invalid 'completed' query parameter");
      return;
    }
    if (!apiKey) {
      setError("Please provide 'apiKey' query parameter");
      return;
    }

    // Make a request to your backend with the correct query + headers
    fetch(`/api/tasks/filter?completed=${completedParam}`, {
      headers: { 'x-api-key': apiKey },
    })
      .then(res => res.json())
      .then(json => {
        if (json.error) {
          setError(json.error);
        } else {
          setTasks(json.data); // store the fetched tasks
        }
      })
      .catch(err => setError(err.message));
  }, [completedParam]); // reruns if 'completed' changes

  // ... (returning the rendered UI)
}

What This Does (Backend-Oriented Breakdown)

  • useState([...]) is like a local variable + setter for the page. We use it here to:

    • Store the fetched task list (tasks)
    • Store error messages (error)
  • useEffect(...) runs some code after the component loads — or when certain data changes. In this case:

    • It runs every time the completed query param changes.
    • It performs validation (e.g., is the param missing? Is the API key included?).
    • If everything looks good, it fetches filtered tasks from your backend using fetch() and stores the results in state.
  • The request uses:

    • The query param completed=true|false to indicate what kind of tasks we want.
    • The API key in the x-api-key header to pass authentication.

Even though you aren’t expected to build this UI yourself, You now know exactly how your /api/tasks/filter endpoint is used in the frontend and you see how the query params from the browser become backend filters. This knowledge will help you debug issues, extend APIs, and build backend logic that cleanly integrates with real-world UIs.

Summary and Practice Preview

In this lesson, you learned how to let users filter tasks by their completion status using query parameters in your API. You saw how to:

  • Read and validate query parameters from the request
  • Use a service function to filter tasks
  • Return the filtered results or clear error messages
  • How the frontend provides those query parameters using buttons that update the URL and include the apiKey for authorization

You are now ready to practice building and testing this feature on your own. In the next exercises, you’ll get hands-on experience with filtering tasks and handling query parameters.

Congratulations on reaching the end of the course! You’ve built a solid foundation in backend development with Next.js. Keep practicing and applying what you’ve learned — you’re well on your way to becoming a backend developer!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal