npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

api-payload-guard

v1.1.0

Published

Zero-dependency TypeScript API payload validator with nested schemas, sanitization, coercion, structured errors, and type inference.

Readme

api-payload-guard

Zero-dependency API payload validation for Node.js and TypeScript. Validate nested request bodies, sanitize unknown fields, normalize input, and return predictable errors without adding a large schema library.

npm version npm downloads CI license

Why use it?

  • Zero runtime dependencies
  • Nested object and array validation with precise paths such as profile.address.zip
  • String, number, integer, boolean, array, object, null, and any types
  • Length, range, pattern, enum, format, and collection constraints
  • Email, URL, UUID, ISO date, and ISO datetime formats
  • Unknown-field reject, strip, or preserve modes
  • Safe opt-in coercion, defaults, normalization, transforms, and custom validators
  • Prototype-pollution key protection and recursion limits
  • Structured errors with custom or localized messages
  • TypeScript inference from schemas
  • Reusable compiled guards and assertion-style validation
  • ESM support for Node.js 18 and newer

Install

npm install api-payload-guard

Quick start

import { defineSchema, guardPayload } from "api-payload-guard";

const createUserSchema = defineSchema({
  email: {
    type: "string",
    required: true,
    trim: true,
    lowercase: true,
    format: "email"
  },
  age: {
    type: "integer",
    min: 13,
    max: 120
  },
  role: {
    type: "string",
    enum: ["user", "admin"]
  },
  profile: {
    type: "object",
    schema: {
      displayName: { type: "string", minLength: 2, maxLength: 50 },
      website: { type: "string", format: "url", nullable: true }
    }
  },
  tags: {
    type: "array",
    maxItems: 10,
    uniqueItems: true,
    items: { type: "string", trim: true, lowercase: true }
  },
  active: {
    type: "boolean",
    default: true
  }
});

const result = guardPayload(
  {
    email: "  [email protected] ",
    age: 24,
    role: "user",
    profile: { displayName: "Dev" },
    tags: [" Node.js ", "TypeScript"]
  },
  createUserSchema
);

if (result.valid) {
  console.log(result.data);
  // email is normalized, tags are normalized, and active is true
} else {
  console.log(result.errors);
}

A successful result is a discriminated union:

{
  valid: true,
  data: { /* validated and normalized fields */ },
  errors: []
}

A failed result never exposes partial data:

{
  valid: false,
  data: null,
  errors: [
    {
      field: "profile.website",
      code: "INVALID_FORMAT",
      expected: "url",
      received: "string",
      message: "profile.website must be a valid url"
    }
  ]
}

Schema reference

Every field requires a type.

| Option | Applies to | Purpose | | --- | --- | --- | | required | all | Reject an omitted or undefined value | | nullable | all | Allow null in addition to the declared type | | default | all | Static value or zero-argument factory used when missing | | enum | all | Allow only values in the supplied array | | coerce | primitives | Override global coercion for this field | | validate | all | Return true, false, or a custom error message | | transform | all | Transform a value before type and constraint validation | | message | all | One custom message or messages keyed by error code | | minLength, maxLength | string | Limit string length | | pattern | string | Match a RegExp or regular-expression string | | format | string | email, url, uuid, iso-date, or iso-datetime | | trim, lowercase, uppercase | string | Normalize the returned string | | min, max | number, integer | Inclusive numeric range | | multipleOf | number, integer | Require a numeric multiple | | finite | number, integer | Reject NaN and infinities by default; set false to allow | | items | array | Validate and transform every array item | | minItems, maxItems | array | Limit array length | | uniqueItems | array | Require structurally unique values | | schema | object | Validate a nested object | | unknownFields | object | Override unknown-field handling for one nested object |

Supported type values are string, number, integer, boolean, array, object, null, and any.

Nested arrays and objects

Rules can be nested to any reasonable depth:

const schema = {
  users: {
    type: "array",
    required: true,
    items: {
      type: "object",
      schema: {
        id: { type: "integer", required: true },
        email: { type: "string", required: true, format: "email" }
      }
    }
  }
};

An invalid email in the second item is reported as users[1].email.

Coercion

Coercion is disabled by default so validation stays predictable. Enable it globally or per field:

guardPayload(payload, schema, { coerce: true });

const schema = {
  page: { type: "integer", coerce: true }
};

Safe coercions include numeric and boolean strings such as "42", "true", and "false", plus numbers or booleans converted to strings. Empty strings are never converted to numbers.

Unknown fields and sanitization

Unknown fields are rejected by default:

guardPayload(payload, schema, { unknownFields: "reject" });

Use strip to return only schema-defined fields, or preserve to keep additional fields:

const result = guardPayload(
  { name: "Ada", isAdmin: true },
  { name: { type: "string" } },
  { unknownFields: "strip" }
);

// result.data is { name: "Ada" }

The keys __proto__, prototype, and constructor are rejected in every mode.

Custom validation and messages

const schema = {
  password: {
    type: "string",
    minLength: 12,
    validate: (value) =>
      /[0-9]/.test(value) || "password must contain a number",
    message: {
      MIN_LENGTH: "Use at least 12 characters"
    }
  }
};

Custom validators and transforms are synchronous. The callback context contains path, root, and parent.

Options

interface GuardOptions {
  unknownFields?: "reject" | "strip" | "preserve";
  abortEarly?: boolean;
  coerce?: boolean;
  maxDepth?: number;
  maxErrors?: number;
  errorMap?: (error: Readonly<ValidationError>) => string;
}
  • unknownFields defaults to reject.
  • abortEarly defaults to false.
  • coerce defaults to false.
  • maxDepth defaults to 32.
  • maxErrors defaults to no limit.
  • errorMap can localize or rewrite messages while retaining the code and field.

API

guardPayload(payload, schema, options?)

Returns GuardResult<InferPayload<typeof schema>>. This is the best choice for HTTP handlers because expected validation failures do not throw.

defineSchema(schema)

An identity helper that preserves literal types for TypeScript inference.

import { defineSchema, guardPayload, type InferPayload } from "api-payload-guard";

const schema = defineSchema({
  email: { type: "string", required: true },
  role: { type: "string", enum: ["user", "admin"] as const }
});

type UserPayload = InferPayload<typeof schema>;

const result = guardPayload(input, schema);
if (result.valid) {
  result.data.email; // string
  result.data.role;  // "user" | "admin" | undefined
}

createPayloadGuard(schema, options?)

Creates a reusable guard for a route or event type. The schema configuration is checked once when the guard is created, and call-time options override the defaults. Treat the schema as immutable after creating the guard.

import { createPayloadGuard } from "api-payload-guard";

const validateUser = createPayloadGuard(userSchema, {
  unknownFields: "strip"
});

const result = validateUser(req.body);

assertPayload(payload, schema, options?)

Returns validated data or throws PayloadValidationError, which contains an errors array.

import {
  assertPayload,
  PayloadValidationError
} from "api-payload-guard";

try {
  const data = assertPayload(input, schema);
} catch (error) {
  if (error instanceof PayloadValidationError) {
    console.error(error.errors);
  }
}

Express example

No framework adapter is required:

app.post("/users", async (req, res) => {
  const result = guardPayload(req.body, createUserSchema, {
    unknownFields: "strip"
  });

  if (!result.valid) {
    return res.status(400).json({
      error: "INVALID_REQUEST_BODY",
      details: result.errors
    });
  }

  const user = await User.create(result.data);
  return res.status(201).json(user);
});

The same pattern works with Fastify, Koa, Hono, Next.js route handlers, webhooks, queues, CLI input, and service boundaries.

Error codes

| Group | Codes | | --- | --- | | Payload | INVALID_PAYLOAD, REQUIRED_FIELD, INVALID_TYPE, UNKNOWN_FIELD, UNSAFE_KEY | | Values | INVALID_ENUM, CUSTOM_VALIDATION, TRANSFORM_FAILED, DEFAULT_FAILED | | Strings | MIN_LENGTH, MAX_LENGTH, PATTERN_MISMATCH, INVALID_FORMAT | | Numbers | NOT_FINITE, MINIMUM, MAXIMUM, MULTIPLE_OF | | Arrays | MIN_ITEMS, MAX_ITEMS, UNIQUE_ITEMS | | Safety | MAX_DEPTH, CIRCULAR_REFERENCE |

Use code for application logic and message for display or logs. Payload values are deliberately omitted from errors to reduce accidental exposure of secrets.

Security scope

Payload validation is one layer of API security. Keep authorization, authentication, output encoding, rate limiting, database constraints, and secret handling in place. See SECURITY.md for vulnerability reporting.

Compatibility

  • Node.js 18 or newer
  • ESM (import) packages
  • JavaScript and TypeScript
  • No runtime dependencies

Contributing

Issues and pull requests are welcome. Read CONTRIBUTING.md before submitting changes.

License

MIT