Introduction: Why Validate Item-Level Requests?

Welcome back! In the previous lesson, you learned how to organize your backend code using a service layer. Now, let’s focus on a key part of building reliable APIs: validation — especially when working with item-level endpoints, such as updating or deleting a single task.

When a user sends data to your API (for example, to update a task), you want to make sure that data is correct and safe before you use it. If you skip validation, your app could break or store bad data. In this lesson, you’ll see how to check incoming data for common problems and return helpful error messages if something is wrong.

This is especially important when working with endpoints like PUT and PATCH, which update existing data. These two methods behave differently:

  • PUT expects all fields to be sent and replaces the entire object.
  • PATCH expects only the fields that should be updated. We’ll validate differently depending on which one the client uses — and that’s where the partial flag comes into play.
From Service Logic to Validation

Before jumping into validation, let’s take a moment to connect what we’ve already built:

  • We have a Task type that defines the structure of our tasks: id, title, content, completed, and optional dueDate.
  • We created a service layer (taskService.ts) to isolate task-related operations like getTaskById and updateTaskById. This helps us keep our API routes clean.
  • Now, we’re focusing on making sure the data sent to those service functions is valid, especially for update operations (PUT and PATCH).

The updateTaskById function accepts a partial flag that determines whether a full (PUT) or partial (PATCH) update is expected. The validation function we’ll build will use this flag to check the data accordingly.

We’ll focus on the updateTaskById function and how to validate the data it receives.

How The `validateTaskPayload` Function Works

To make sure only valid data is accepted, we use a function called validateTaskPayload. This function checks the incoming data and returns a list of errors if something is wrong.

Here’s the function:

export function validateTaskPayload(
  payload: any,
  partial = false
): string[] {
  const errors: string[] = [];

  if (!payload || typeof payload !== 'object') {
    return ['Invalid request body. Expected a JSON object.'];
  }

  const allowedFields = ['title', 'content', 'completed', 'dueDate'];
  for (const key of Object.keys(payload)) {
    if (!allowedFields.includes(key)) {
      errors.push(`Unexpected field: '${key}' is not allowed.`);
    }
  }

  if (!partial) {
    if (!('title' in payload)) errors.push("'title' is required.");
    if (!('content' in payload)) errors.push("'content' is required.");
    if (!('completed' in payload)) errors.push("'completed' is required.");
  }

  if ('title' in payload && typeof payload.title !== 'string') {
    errors.push("'title' must be a string.");
  }

  if ('content' in payload && typeof payload.content !== 'string') {
    errors.push("'content' must be a string.");
  }

  if ('completed' in payload && typeof payload.completed !== 'boolean') {
    errors.push("'completed' must be a boolean value.");
  }

  if ('dueDate' in payload) {
    if (payload.dueDate !== undefined && typeof payload.dueDate !== 'string') {
      errors.push("'dueDate' must be a string in ISO format.");
    } else if (typeof payload.dueDate === 'string') {
      const date = new Date(payload.dueDate);
      if (isNaN(date.getTime())) {
        errors.push("'dueDate' must be a valid date string.");
      }
    }
  }

  return errors;
}

Let’s break down what this function does:

  • Checks if the payload is an object:
    If the data sent is not a JSON object, it returns an error.

  • Checks for unexpected fields:
    Only title, content, completed, and dueDate are allowed. If there’s anything else, it adds an error.

  • Checks for required fields:
    If partial is false, all fields except dueDate are required.

  • Checks data types:

    • title and content must be strings.
    • completed must be a boolean (true or false).
    • dueDate must be a string in date format (if provided).

Example:

// Simulating a PATCH request
const errors = validateTaskPayload(
  {
    title: 123,
    content: "Do homework",
    completed: "yes"
  },
  true // partial update
);
// errors: ["'title' must be a string.", "'completed' must be a boolean value."]

This helps us catch mistakes before they cause problems in our app. Notice how multiple validation errors are returned at once. This design improves the developer experience — the client can fix all the issues in one go rather than having to retry multiple times for each error.

Applying Validation To Update Endpoints

Now, let’s see how to use this validation function when updating a task. In our service layer, the updateTaskById function uses validateTaskPayload to check the data before making any changes.

Here’s the relevant part of the code:

export const updateTaskById = (
  id: number,
  data: any,
  partial = true
): { task?: Task; error?: string } => {
  const index = tasks.findIndex(t => t.id === id);
  if (index === -1) return { error: 'Task not found' };

  const errors = validateTaskPayload(data, partial);
  if (errors.length > 0) {
    return { error: errors.join('; ') };
  }

  if (partial) {
    if ('title' in data) tasks[index].title = data.title;
    if ('content' in data) tasks[index].content = data.content;
    if ('completed' in data) tasks[index].completed = data.completed;
    if ('dueDate' in data) tasks[index].dueDate = data.dueDate;
  } else {
    tasks[index].title = data.title;
    tasks[index].content = data.content;
    tasks[index].completed = data.completed;
    tasks[index].dueDate = data.dueDate;
  }

  return { task: tasks[index] };
};

What happens here?

  1. The function looks for the task by its id.
  2. It validates the incoming data using validateTaskPayload.
  3. If there are errors, it returns them and does not update the task.
  4. If the data is valid, it updates only the fields provided (for partial updates).

Example:

// PATCH-style usage (partial = true), note that it's set to true by default
const result = updateTaskById(1, { completed: "yes" }, true);
// result: { error: "'completed' must be a boolean value." }

If the data is valid:

// PATCH-style usage (partial = true)
const result = updateTaskById(1, { completed: true }, true);
// result: { task: { id: 1, title: "...", content: "...", completed: true, dueDate: "...?" } }
Returning Helpful Error Messages

When validation fails, it’s important to let the client know exactly what went wrong. Our service returns an error message that lists all the problems found.

Example of a bad request:

// Simulating a full update (PUT behavior)
const result = updateTaskById(
  2,
  {
    title: 123,
    content: "Updated content",
    completed: false,
    extra: "oops"
  },
  false
);
// result: { error: "'title' must be a string.; Unexpected field: 'extra' is not allowed." }

Example of a good request:

// PATCH-style usage (partial = true)
const result = updateTaskById(2, { title: "Read a book" }, true);
// result: { task: { id: 2, title: "Read a book", content: "...", completed: false, dueDate: "...?" } }

By returning clear error messages, you help users of your API fix their mistakes quickly.

Summary And Practice Preview

In this lesson, you learned how to validate data for item-level endpoints in your Task Manager API. You saw how the validateTaskPayload function checks for required fields, correct data types, and unexpected fields. You also learned how to use this function in your update endpoint and how to return helpful error messages when something goes wrong.

Next, you’ll get to practice writing and using validation in your own code. This will help you build more reliable and user-friendly APIs. Good luck, and see you in the practice exercises!

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