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

@dsyves/form-schema

v0.3.1

Published

Server-Driven UI form schema generator for NestJS. Reads UI decorators on DTO classes and produces typed, frontend-ready JSON form schemas. Optionally integrates with class-validator to automatically infer validation rules.

Downloads

526

Readme

@dsyves/form-schema

npm version npm downloads License: MIT TypeScript NestJS LinkedIn

Server-Driven UI form schema generator for NestJS.
Decorate your DTO properties — the library generates the JSON schema for your Frontend.


The Problem

Complex systems have forms that change constantly. Adding a new field or changing a required rule usually means touching both the Backend (DTOs) and the Frontend (React/Remix screens), doubling effort and risking inconsistencies.

The Solution (SDUI)

Invert control. The Frontend stops hardcoding form rules. The Backend becomes the Single Source of Truth. A single endpoint returns a JSON "schema" of the screen, and the Frontend just renders it.


Installation

npm install @dsyves/form-schema reflect-metadata

⚠️ Make sure reflect-metadata is imported once at the top of your application entry point (e.g., main.ts):

import 'reflect-metadata';

Also enable these flags in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": false
  }
}

Quick Start

1. Register the module

// app.module.ts
import { SchemaModule } from '@dsyves/form-schema';

@Module({
  imports: [SchemaModule.forRoot()],
})
export class AppModule {}

2. Decorate your DTO

// product.dto.ts
import { UIString, UINumber, UISelect } from '@dsyves/form-schema';

type AppModes = 'create' | 'update' | 'view' | 'audit';

export class ProductDto {
  @UIString<AppModes>({
    label: 'Product Code',
    editableIn: ['create'],   // disabled in update/view/audit
    required: true,
  })
  code: string;

  @UINumber<AppModes>({
    label: 'Weight (kg)',
    required: true,
    min: 0,
    max: 999,
  })
  weight: number;

  @UISelect<AppModes>({
    label: 'Category',
    options: [
      { label: 'Electronics', value: 'electronics' },
      { label: 'Furniture', value: 'furniture' },
    ],
  })
  category: string;
}

3. Generate the schema in your Controller

// form.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { SchemaGeneratorService } from '@dsyves/form-schema';
import { ProductDto } from './product.dto';

@Controller('forms')
export class FormController {
  constructor(private readonly schemaGenerator: SchemaGeneratorService) {}

  @Get('product')
  getSchema(@Query('mode') mode: string) {
    return this.schemaGenerator.generate(ProductDto, {
      currentMode: mode ?? 'create',
    });
  }
}

4. The JSON output (what your React/Remix receives)

{
  "formName": "ProductDto",
  "requestedMode": "update",
  "fields": [
    {
      "name": "code",
      "type": "string",
      "label": "Product Code",
      "disabled": true,
      "validations": { "required": true }
    },
    {
      "name": "weight",
      "type": "number",
      "label": "Weight (kg)",
      "disabled": false,
      "validations": { "required": true, "min": 0, "max": 999 }
    },
    {
      "name": "category",
      "type": "select",
      "label": "Category",
      "disabled": false,
      "options": [
        { "label": "Electronics", "value": "electronics" },
        { "label": "Furniture", "value": "furniture" }
      ]
    }
  ]
}

class-validator Integration (v0.3+)

Starting from v0.3, the library automatically reads validation constraints registered by class-validator decorators and injects the corresponding rules into the generated schema — without any extra configuration.

Why this matters

Before v0.3, you had to duplicate validation logic:

// ❌ Before — rules written twice
@UIPassword({
  label: 'Password',
  required: true,
  minLength: 8,
  pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_]).{8,}$",
})
@IsStrongPassword({ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1 })
password: string;

Now you write the rule once:

// ✅ After — DRY, single source of truth
@UIPassword({ label: 'Password', required: true })
@IsStrongPassword({ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1 })
password: string;

Setup

class-validator is an optional peer dependency. Install it only if your project uses it:

npm install class-validator

No other configuration is needed. The library detects class-validator automatically at runtime via a safe dynamic require(). If it is not installed, the schema generator works exactly as before.

Merge priority: explicit always wins

Values explicitly set on the UI decorator always take precedence over inferred values. This lets you override any inferred rule when needed:

@UIPassword({
  label: 'Password',
  minLength: 16,                   // overrides inferred 8 from @IsStrongPassword
  pattern: '^MyCustomRegex.{16,}$', // overrides the auto-generated pattern
})
@IsStrongPassword({ minLength: 8 })
password: string;

Decorator mapping reference

✅ Fully mapped

| class-validator decorator | Inferred validations field | Notes | |---|---|---| | @IsNotEmpty() | required: true | — | | @IsDefined() | required: true | — | | @IsOptional() | required: false | Always wins; clears any required: true from other decorators | | @MinLength(n) | minLength: n | — | | @MaxLength(n) | maxLength: n | — | | @Length(min, max) | minLength, maxLength | — | | @Min(n) | min: n | — | | @Max(n) | max: n | — | | @IsPositive() | min: 1 | HTML min is inclusive | | @IsNegative() | max: -1 | — | | @IsLatitude() | min: -90, max: 90 | — | | @IsLongitude() | min: -180, max: 180 | — | | @IsInt() | step: 1 | Restricts <input type="number"> to integers | | @ArrayMinSize(n) | minLength: n | — | | @ArrayMaxSize(n) | maxLength: n | — | | @ArrayNotEmpty() | required: true | — | | @Matches(/regex/) | pattern from RegExp.source | Always overwrites inferred patterns | | @Contains("seed") | pattern: ^.*seed.*$ | Seed is regex-escaped | | @IsIn(["a","b"]) | pattern: ^(a\|b)$ | Values are regex-escaped | | @IsEmail() | pattern (RFC 5321) | Can be overridden via @UIEmail({ pattern }) | | @IsUrl() | pattern (http/https/ftp) | — | | @IsUUID() / @IsUUID("4") | pattern by version | Supports v3, v4, v5, or any | | @IsIP() / @IsIP("4") / @IsIP("6") | pattern by version | — | | @IsStrongPassword(opts?) | minLength + pattern with look-aheads | Pattern is derived from the options you pass | | @IsAlpha() | pattern: ^[a-zA-Z]+$ | — | | @IsAlphanumeric() | pattern: ^[a-zA-Z0-9]+$ | — | | @IsNumberString() | pattern (int or decimal) | — | | @IsDecimal() | pattern (decimal only) | — | | @IsLowercase() | pattern | — | | @IsUppercase() | pattern | — | | @IsHexadecimal() | pattern | — | | @IsHexColor() | pattern (#rgb / #rrggbb) | — | | @IsOctal() | pattern | — | | @IsBase64() | pattern | — | | @IsMongoId() | pattern (24-char hex) | — | | @IsJWT() | pattern (3 base64url segments) | — | | @IsDataURI() | pattern | — | | @IsFQDN() | pattern (domain name) | — | | @IsISO8601() | pattern (date/datetime) | — | | @IsRgbColor() | pattern (rgb() / rgba()) | — | | @IsHSL() | pattern (hsl() / hsla()) | — | | @IsAscii() | pattern (printable ASCII) | — | | @IsIBAN() | pattern (rough IBAN format) | — | | @IsPhoneNumber() | pattern (E.164) | Generic; not locale-specific | | @IsPostalCode() | pattern (4–10 digits) | Generic; not locale-specific | | @IsMimeType() | pattern | — | | @IsHash("sha256") | pattern by algorithm | Supports md5, sha1, sha256, sha512, and more | | @IsCreditCard() | pattern (format only) | Luhn check-digit stays on the backend | | @IsISBN() / @IsISBN(10) / @IsISBN(13) | pattern by version | Format only; check-digit stays on the backend |

⏭️ No-op (intentionally not mapped)

| class-validator decorator | Reason | |---|---| | @IsString() | Type hint only — all form fields are strings by nature | | @IsBoolean() | Type hint — handled by @UICheckbox | | @IsNumber() | Type hint — @Min/@Max cover numeric validation | | @IsDate() | Type hint — handled by @UIDate | | @IsJson() | JSON validation requires runtime parsing; no useful regex | | @NotContains() | Negative containment has no HTML attribute equivalent | | @IsNotIn() | Negative set exclusion has no HTML attribute equivalent | | @IsEmpty() | Antonym of required — ambiguous in form context | | @IsPassportNumber() | Hundreds of country-specific formats; too risky to generalize |


Available UI Decorators

| Decorator | HTML Element | Extra Options | | --- | --- | --- | | @UIString() | <input type="text"> | — | | @UINumber() | <input type="number"> | — | | @UIEmail() | <input type="email"> | — | | @UIPassword() | <input type="password"> | — | | @UIDate() | <input type="date"> | withTime: truedatetime-local | | @UICheckbox() | <input type="checkbox"> | — | | @UIRadio() | <input type="radio"> | options: UISelectOption[] (required) | | @UISelect() | <select> | options: UISelectOption[] (required) | | @UITextarea() | <textarea> | — | | @UIFile() | <input type="file"> | accept, maxSizeMb, multiple |


Base Options (all UI decorators)

interface UIFieldOptions<TMode extends string = 'create' | 'update' | 'view'> {
  label: string;
  editableIn?: TMode[];    // modes where this field is editable
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  min?: number | string;
  max?: number | string;
  pattern?: string;        // Regex string (HTML5 pattern attribute)
  placeholder?: string;
}

Generic Modes (The Key Design Decision)

Instead of a closed enum, the library uses Generic Literal Types. This means you can define your own custom modes and the TypeScript compiler will validate them everywhere:

// Your project defines its own modes
type AppModes = 'create' | 'update' | 'view' | 'audit';

export class ShipmentDto {
  @UIString<AppModes>({
    label: 'Tracking Code',
    editableIn: ['create', 'audit'],  // ✅ TypeScript autocomplete!
    // editableIn: ['wrong_mode'],    // ❌ compile-time error
  })
  trackingCode: string;
}

Advanced Example

With class-validator (recommended)

import { UIEmail, UIPassword, UIString } from '@dsyves/form-schema';
import {
  IsEmail, IsNotEmpty, IsStrongPassword,
  IsOptional, Length, Matches,
} from 'class-validator';

export class CreateUserDto {
  @UIEmail({ label: 'E-mail' })
  @IsEmail()
  @IsNotEmpty()
  // ↑ required:true and pattern inferred automatically
  email: string;

  @UIPassword({ label: 'Password' })
  @IsStrongPassword({ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1 })
  // ↑ minLength:8 and look-ahead pattern inferred automatically
  password: string;

  @UIString({ label: 'License Plate', placeholder: 'ABC1D23' })
  @Matches(/^[A-Z]{3}\d[A-Z\d]\d{2}$/)
  @Length(7, 7)
  // ↑ pattern and minLength/maxLength inferred automatically
  licensePlate: string;

  @UIString({ label: 'Nickname' })
  @IsOptional()
  // ↑ required:false inferred; field is fully optional
  nickname?: string;
}

Generated JSON for mode=create:

{
  "formName": "CreateUserDto",
  "requestedMode": "create",
  "fields": [
    {
      "name": "email",
      "type": "email",
      "label": "E-mail",
      "disabled": false,
      "validations": {
        "required": true,
        "pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
      }
    },
    {
      "name": "password",
      "type": "password",
      "label": "Password",
      "disabled": false,
      "validations": {
        "minLength": 8,
        "pattern": "^(?=(.*[a-z]){1,})(?=(.*[A-Z]){1,})(?=(.*\\d){1,})(?=(.*[\\W_]){1,}).{8,}$"
      }
    },
    {
      "name": "licensePlate",
      "type": "string",
      "label": "License Plate",
      "disabled": false,
      "placeholder": "ABC1D23",
      "validations": {
        "minLength": 7,
        "maxLength": 7,
        "pattern": "^[A-Z]{3}\\d[A-Z\\d]\\d{2}$"
      }
    },
    {
      "name": "nickname",
      "type": "string",
      "label": "Nickname",
      "disabled": false,
      "validations": { "required": false }
    }
  ]
}

Without class-validator

You can still pass all rules manually via the UI decorator — everything works exactly as before:

export class ShipmentDto {
  @UIString<AppModes>({
    label: 'License Plate',
    required: true,
    minLength: 7,
    maxLength: 7,
    pattern: '^[A-Z]{3}[0-9][A-Z0-9][0-9]{2}$',
    placeholder: 'ABC1D23',
    editableIn: ['create', 'audit'],
  })
  licensePlate: string;

  @UIFile<AppModes>({
    label: 'Invoice',
    accept: ['.pdf', 'image/jpeg', 'image/png'],
    maxSizeMb: 5,
    multiple: false,
    editableIn: ['create', 'audit'],
  })
  invoiceFile: any;
}

Architecture

src/
├── types.ts                    # All interfaces & type definitions
├── decorators.ts               # @UIString, @UINumber, @UISelect, etc.
├── class-validator-bridge.ts   # Optional class-validator metadata reader
├── schema-generator.service.ts # Core logic: reads metadata → UIFormSchema
├── schema.module.ts            # NestJS Dynamic Module (forRoot / forRootAsync)
└── index.ts                    # Public API barrel

Changelog

v0.3.0

  • New: Optional integration with class-validator. The SchemaGeneratorService now automatically reads constraints from class-validator decorators (@IsEmail, @IsStrongPassword, @MinLength, @Matches, etc.) and injects the corresponding validation rules into the generated schema.
  • New: step field added to UIValidationRules (mapped from @IsInt()).
  • New: inferValidationsFromClassValidator() and buildStrongPasswordPattern() exported as public utilities.
  • Breaking: None — fully backward compatible. If class-validator is not installed, behaviour is identical to v0.2.

Author

Created by Yves de Sá Barbosa.

License

MIT