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

@quadcore-lib/input-validation

v0.3.0

Published

Schemas de validación de inputs isomórficos (zod) compartidos entre frontend y backend de QuadCore.

Readme

@quadcore-lib/input-validation

Schemas de validación de inputs isomórficos (una sola definición con zod) compartidos entre el frontend (React) y el backend (NestJS) de QuadCore. Una regla se escribe una vez y la usan las dos puntas.

Validación ≠ anti-inyección. Este paquete valida forma, tipo y allowlist del input y acota la superficie de entrada (largos máximos, caracteres permitidos, .strict() para descartar campos desconocidos). No previene por sí solo XSS, SQLi, OS command injection ni SSTI — eso se maneja en el sink: queries parametrizadas (TypeORM), escape en el render (React), execFile sin shell, etc. Usar ambas capas.

Instalación

npm install @quadcore-lib/input-validation

Uso

Definición única

import { z } from 'zod';
import { createAddressSchema, type CreateAddressInput } from '@quadcore-lib/input-validation';

Backend (NestJS) — con ZodValidationPipe de @quadcore-lib/core-server

import { ZodValidationPipe } from '@quadcore-lib/core-server';
import { createAddressSchema, type CreateAddressInput } from '@quadcore-lib/input-validation';

@Post()
create(@Body(new ZodValidationPipe(createAddressSchema)) dto: CreateAddressInput) { /* ... */ }

Por defecto el 400 devuelve un mensaje genérico ({ message: 'Datos inválidos' }), sin filtrar el contrato de validación a tráfico público. El detalle por campo lo resuelve el cliente con su propio safeParse. En endpoints internos/admin podés pedir el detalle con new ZodValidationPipe(schema, { exposeIssues: true }).

Frontend (React) — con react-hook-form

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createAddressSchema, type CreateAddressInput } from '@quadcore-lib/input-validation';

const form = useForm<CreateAddressInput>({ resolver: zodResolver(createAddressSchema) });
// Validación en cliente + tipos, con las mismas reglas que valida el backend.

Sin framework

const result = createAddressSchema.safeParse(input);
if (!result.success) { /* result.error.issues */ }

API

Primitivas

| Export | Tipo | Descripción | |---|---|---| | alphanumeric({min?,max?}) | factory | Allowlist estricta A-Za-z0-9. | | pattern(regex, msg?, {min?,max?}) | factory | Allowlist configurable por regex propia. | | safeText({min?,max?,multiline?}) | factory | Texto libre con tope de largo, sin caracteres de control. multiline: true permite \n/\r/\t (para <textarea>). | | slug | schema | Slug en minúsculas (a-z, 0-9, guiones). | | email | schema | Email normalizado a minúsculas (máx 254). | | uuid | schema | UUID. | | postalCode | schema | Código postal genérico (letras, dígitos, espacios, guiones; 2–12). Default de los dominios. | | cuit | schema | CUIT/CUIL AR con dígito verificador; normaliza guiones. | | postalCodeAR | schema | Código postal AR (4 dígitos o CPA). Opt-in para proyectos que exijan formato AR. | | phone | schema | Teléfono internacional, formato E.164 flexible (opcional +, 8–15 dígitos). Solo forma, no verifica existencia. | | phoneAR | schema | Alias de phone (la regex nunca fue específica de AR). | | password | schema | 8–128, con minúscula + mayúscula + dígito. | | money, percentage, positiveInt, nonNegativeInt | schema | Numéricos comunes. |

Schemas de dominio

| Export | Descripción | |---|---| | createAddressSchema / CreateAddressInput | Alta de dirección (piloto). .strict(). | | updateAddressSchema / UpdateAddressInput | Actualización parcial de dirección. | | listUsersQuerySchema / ListUsersQueryInput | Query de listado de usuarios. | | updateRoleSchema / UpdateRoleInput | Cambio de rol de usuario. | | updateStatusSchema / UpdateStatusInput | Activar/desactivar cuenta de usuario. | | createCategorySchema / CreateCategoryInput | Alta de categoría. | | updateCategorySchema / UpdateCategoryInput | Actualización parcial de categoría. | | listCategoriesQuerySchema / ListCategoriesQueryInput | Query de listado de categorías. | | createProductSchema / CreateProductInput | Alta de producto. | | updateProductSchema / UpdateProductInput | Actualización parcial de producto. | | adjustStockSchema / AdjustStockInput | Ajuste de stock (delta/set). | | listProductsQuerySchema / ListProductsQueryInput | Query de listado de productos. | | createOrderSchema / CreateOrderInput | Alta de orden (precio/envío/cupón resueltos server-side). | | listOrdersQuerySchema / ListOrdersQueryInput | Query de listado de órdenes. | | updateOrderStatusSchema / UpdateOrderStatusInput | Cambio de estado de orden. ORDER_STATUSES. | | createCouponSchema / CreateCouponInput | Alta de cupón. COUPON_TYPES. | | updateCouponSchema / UpdateCouponInput | Actualización parcial de cupón. | | validateCouponSchema / ValidateCouponInput | Validación pública de cupón en checkout. | | createPaymentSchema / CreatePaymentInput | Alta de pago (monto derivado server-side). | | createShippingMethodSchema / CreateShippingMethodInput | Alta de método de envío. | | updateShippingMethodSchema / UpdateShippingMethodInput | Actualización parcial de método de envío. | | createTicketSchema / CreateTicketInput | Alta pública de ticket. | | updateTicketSchema / UpdateTicketInput | Gestión admin de ticket. TICKET_STATUSES. | | listTicketsQuerySchema / ListTicketsQueryInput | Query de listado de tickets. | | upsertSettingSchema / UpsertSettingInput | Alta/actualización de setting. SETTING_TYPES. | | createNotificationSchema / CreateNotificationInput | Alta admin de notificación. |

Los *_STATUSES/*_TYPES (ORDER_STATUSES, COUPON_TYPES, TICKET_STATUSES, SETTING_TYPES) se definen acá y la entity de cada *-server los reexporta (export { ORDER_STATUSES } from '@quadcore-lib/input-validation'). La dependencia va en esa dirección — los *-server dependen de input-validation, no al revés — así que hay una sola lista y no dos que se puedan desincronizar. Agregar o renombrar un estado se hace en un solo lugar.

Campos opcionales y la cadena vacía

Dos helpers, porque "" no significa lo mismo en todos lados:

| Helper | Qué hace con "" | Dónde se usa | |---|---|---| | optionalText({ max }) | lo preserva | texto libre de un body (description, notes, label): mandar "" es como un formulario limpia un campo que ya tenía valor | | blankToUndefined(schema) | lo vuelve undefined | query strings (?search=, ?categoryId=) y campos de body de tipo cerrado (email, uuid, enum), donde "" quiere decir "no lo mandé" |

Un safeText() pelado arranca en min: 1, así que sigue rechazando "" — que es lo correcto para un campo obligatorio.

Schema vs DTO

Con class-validator, el DTO era una clase que hacía dos cosas a la vez: definía la forma/tipo del body y cargaba las reglas de validación como decoradores (@IsString(), etc.). Forma y validación fusionadas en un artefacto escrito a mano.

Con zod eso se separa en una sola fuente: el schema valida en runtime y de él se infiere el tipo (type CreateAddressInput = z.infer<typeof createAddressSchema>). Ya no hace falta una clase DTO: lo único que queda es ese tipo inferido, que sale gratis del schema. Por eso en los paquetes migrados no verás CreateAddressDto (clase), sino CreateAddressInput (tipo derivado del schema).

  • Validación de input (runtime) → la hace el schema.
  • "DTO" en el sentido de "forma de lo que viaja" → es el tipo inferido, sin duplicación.
  • Los *.dto.ts con class-validator que todavía existan en otros paquetes son deuda de migración, no el patrón nuevo.

Manejo de errores: público vs cliente

Son dos mensajes distintos, a propósito:

| Capa | Quién lo ve | Qué dice | |---|---|---| | Respuesta HTTP 400 (backend) | Tráfico público (cualquiera que pegue a la API) | { message: "Datos inválidos", fields: ["postalCode", ...] }. Los nombres de los campos que fallaron (para que un cliente sin el schema sepa qué revisar), sin los mensajes de validación. | | Errores inline del form (frontend) | La persona que completa el formulario | Descriptivo por campo ("Código postal inválido"). Se calcula en el browser con schema.safeParse — nunca sale del server. |

El detalle textual lo resuelve el cliente con el mismo schema. Endpoints internos/admin que quieran los mensajes en la respuesta: new ZodValidationPipe(schema, { exposeIssues: true }) (agrega issues con path + message).

Recordar: esto valida forma/tipo/allowlist. Las inyecciones (XSS, SQLi, OSi, SSTI) se previenen en el sink, no acá — ver la nota del inicio.

Requisitos

  • zod >= 3.23 (dependencia directa del paquete).
  • Backend: ZodValidationPipe de @quadcore-lib/core-server.
  • Frontend: schema.safeParse directo, o react-hook-form + @hookform/resolvers si el form crece.

Build: CJS + ESM

El paquete es isomórfico (lo consumen *-server en CommonJS y account en ESM), así que compila dos veces desde la misma fuente:

dist/
  cjs/    ← module: commonjs   (require/module.exports)
  esm/    ← module: ESNext     (import/export nativo)
  types/  ← .d.ts, generado una sola vez desde la build CJS

package.json declara exports condicional para que cada consumidor resuelva el que le corresponde (require → dist/cjs, import → dist/esm, default → dist/cjs para cualquier resolver con otras condiciones, como jsdom).

scripts/postbuild.mjs (los dos pasos corren sobre dist/, el src queda intacto):

  1. Escribe dist/cjs/package.json (type: commonjs) y dist/esm/package.json (type: module). Node decide el tipo de módulo por el package.json más cercano al .js que carga, y el raíz no declara "type".
  2. Le agrega la extensión .js a los imports relativos del emitido en dist/esm. El src usa moduleResolution: "bundler", así que tsc los emite sin extensión — un bundler los resuelve, pero el loader ESM de Node exige la extensión explícita y sin esto tiraría ERR_MODULE_NOT_FOUND.

Se hace en el postbuild y no escribiendo .js en los imports del src a propósito: de esa forma el código fuente no se ensucia, jest sigue resolviendo sin moduleNameMapper, y cualquier archivo nuevo del paquete entra solo sin que haya que acordarse de nada.

Verificado en el build: require('@quadcore-lib/input-validation') y import '@quadcore-lib/input-validation' (Node ESM nativo, sin bundler) resuelven los dos.

Pendiente conocido: dist/types se emite una sola vez desde la build CJS, así que un consumidor con moduleResolution: "node16"/"nodenext" va a ver tipos en formato CJS colgando de la condición import. Hoy no molesta (los *-server usan node y los paquetes de frontend bundler), pero si aparece un consumidor nodenext hay que emitir tipos por build.