Validating Incoming Data

Introduction: Why Validate Incoming Data?

In the previous lesson, you learned how to build consistent API responses so that every request returned a predictable structure. In this lesson, we’ll take the next important step toward building a reliable backend — validating incoming data.

When your API receives data from clients (for example, creating or updating a user), you can’t assume that data is valid or complete. Someone might send:

  • an empty name,
  • an invalid email address,
  • or even omit required fields entirely.

If your backend doesn’t validate data before using it, it could save bad information to memory or a database, crash unexpectedly, or behave in unsafe ways.

Validation acts as a gatekeeper, checking every incoming request and ensuring only clean, well-structured data makes it into your system.

By the end of this lesson, you’ll know how to:

  • Use a centralized validation module (app/lib/validation.ts),
  • Integrate it into your API routes,
  • And observe validation behavior directly in the preview UI.

Understanding the Purpose of Validation

Data validation means verifying that incoming information matches your expectations before you use it.

In your API, this applies to any operation where users send data — especially POST, PUT, and PATCH requests that create or update users.

Validation answers these questions:

  • Is this field required?
  • Is the value the correct type (string, number, boolean)?
  • Does it match allowed values (like "admin" or "user" for a role)?
  • Does it have the right format (like an email address)?

For example:

  • A name should not be empty.
  • An email should look like "person@example.com".
  • The role should be one of a specific set of roles.
  • The isActive flag should be a boolean, not a string like "yes".

By centralizing this logic in one file, you ensure all routes follow the same rules and produce clear error messages that both developers and users can understand.

The Validation Module: app/lib/validation.ts

Your validation logic lives in a single, reusable module that defines rules for checking user input. Let’s look at the core of this file.

TypeScript
const ALLOWED_ROLES = ["user", "admin", "moderator"] as const;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const VALID_KEYS = new Set(["name", "email", "role", "isActive"]);

type ValidationOptions = { requireAll?: boolean };

export function validateUserPayload(
  payload: unknown,
  opts: ValidationOptions = {}
): string[] {
  const errors: string[] = [];
  const { requireAll = false } = opts;

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

  const record = payload as Record<string, unknown>;
  const has = (k: string) => Object.prototype.hasOwnProperty.call(record, k);

  if (!requireAll) {
    const hasValidField = Array.from(VALID_KEYS).some((k) => has(k));
    if (!hasValidField) {
      errors.push("Provide at least one of name, email, role, or isActive.");
    }
  }

  if (requireAll || has("name")) {
    const rawName = record.name;
    if (typeof rawName !== "string" || rawName.trim() === "") {
      errors.push("'name' must be a non-empty string.");
    }
  }

  if (requireAll || has("email")) {
    const rawEmail = record.email;
    if (typeof rawEmail !== "string" || rawEmail.trim() === "") {
      errors.push("'email' must be a non-empty string.");
    } else if (!EMAIL_PATTERN.test(rawEmail.trim())) {
      errors.push("'email' must be a valid email address.");
    }
  }

  if (has("role")) {
    const rawRole = record.role;
    if (typeof rawRole !== "string" || rawRole.trim() === "") {
      errors.push("'role' must be a non-empty string if provided.");
    } else {
      const normalizedRole = rawRole.trim();
      if (!ALLOWED_ROLES.includes(normalizedRole as (typeof ALLOWED_ROLES)[number])) {
        errors.push(`'role' must be one of: ${ALLOWED_ROLES.join(", ")}.`);
      }
    }
  }

  if (has("isActive")) {
    const rawIsActive = record.isActive;
    if (typeof rawIsActive !== "boolean") {
      errors.push("'isActive' must be a boolean.");
    }
  }

  return errors;
}
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