DEV Community

Cover image for Fortify Schema
ProCoder 😌
ProCoder 😌

Posted on

Fortify Schema

Fortify Schema

TypeScript-First Validation Library with Intuitive Syntax
Report any bugs to Nehonix-Team via: team@nehonix.space

Fortify Schema, is a powerful TypeScript-first schema validation library that combines the familiarity of TypeScript interfaces with runtime validation and automatic type inference.

✨ Key Features

  • 🎯 Interface-like Syntax β€” Define schemas using familiar TypeScript interface syntax
  • ⚑ Runtime Type Inference β€” Validated data is automatically typed without manual casting
  • 🚫 Non-Empty Validation β€” New "!" syntax for non-empty strings and non-zero numbers
  • πŸ”§ Rich Constraints β€” Support for string length, number ranges, arrays, unions, and constants
  • πŸ› οΈ Schema Utilities β€” Transform schemas with partial(), omit(), and extend() methods
  • 🎨 VSCode Integration β€” Dedicated extension with syntax highlighting and IntelliSense
  • πŸ“¦ Zero Dependencies β€” Lightweight and performant

πŸš€ Quick Start

Installation

npm install fortify-schema
# or
yarn add fortify-schema
# or
pnpm add fortify-schema
Enter fullscreen mode Exit fullscreen mode

Requirements: TypeScript 4.5+ and Node.js 14+

Basic Usage

import { Interface } from 'fortify-schema';

// Define your schema
const UserSchema = Interface({
  id: "number!",                 // Non-zero number (basic type with "!")
  name: "string(2,50)",          // String with length constraints (2-50 chars)
  email: "email",                // Email validation (no "!" available)
  age: "number(18,120)?",        // Optional age between 18-120
  bio: "string!?",               // Optional, but if provided must be non-empty
  tags: "string[](1,10)?",       // Optional array of 1-10 strings
  status: "active|inactive",     // Union type
  role: "=admin",                // Constant value
});

// Valid data
const userData = {
  id: 1,                         // βœ… Non-zero number
  name: "Jane Doe",              // βœ… String within length constraints
  email: "hello@example.com",    // βœ… Valid email format
  status: "active",
  role: "admin",
};

// Invalid data examples
const invalidData = {
  id: 0,                         // ❌ Fails: number! rejects 0
  name: "J",                     // ❌ Fails: string(2,50) requires minimum 2 chars
  email: "invalid-email",        // ❌ Fails: invalid email format
  status: "active",
  role: "admin",
};

const result = UserSchema.safeParse(userData);

if (result.success) {
  // βœ… Data is valid and properly typed
  console.log("Welcome,", result.data.name);
  console.log("User ID:", result.data.id); // TypeScript knows this is a number
} else {
  // ❌ Handle validation errors
  console.error("Validation failed:", result.error.issues);
}
Enter fullscreen mode Exit fullscreen mode

πŸ“‹ Schema Syntax Reference

Primitive Types

const schema = Interface({
  text: "string",           // Any string (including empty "")
  count: "number",          // Any number (including 0)
  flag: "boolean",          // Boolean value
  timestamp: "date",        // Date object
  contact: "email",         // Valid email format
  website: "url",           // Valid URL format
});
Enter fullscreen mode Exit fullscreen mode

Non-Empty/Non-Zero Values (New!)

const schema = Interface({
  // "!" syntax - ONLY for basic string and number types
  name: "string!",          // Non-empty string (rejects "")
  count: "number!",         // Non-zero number (rejects 0)

  // Other types don't support "!" - they have their own validation logic
  email: "email",           // ❌ Cannot do "email!" - not supported
  url: "url",               // ❌ Cannot do "url!" - not supported  
  date: "date",             // ❌ Cannot do "date!" - not supported

  // Optional variants
  title: "string!?",        // Optional, but if provided must be non-empty
  score: "number!?",        // Optional, but if provided must be non-zero
});
Enter fullscreen mode Exit fullscreen mode

Constrained Types

const schema = Interface({
  // Constraint syntax - for length/range validation
  username: "string(3,20)",      // String with length 3-20
  age: "number(0,150)",          // Number between 0-150
  score: "number(0,)",           // Number >= 0
  code: "string(,10)",           // String with max length 10
});

// Non-empty validation - alternative to constraints
const nonEmptySchema = Interface({
  title: "string!",              // Non-empty string (simpler than "string(1,)")
  quantity: "number!",           // Non-zero number
});

// IMPORTANT: Choose ONE approach - cannot combine both:
// βœ… "string(1,100)"     - Use constraints for length validation
// βœ… "string!"           - Use for simple non-empty validation  
// ❌ "string!(1,100)"    - INVALID: Cannot combine both syntaxes
Enter fullscreen mode Exit fullscreen mode

Arrays

const schema = Interface({
  tags: "string[]",              // Array of strings
  scores: "number[](1,5)",       // 1-5 numbers
  items: "string[](,10)",        // Max 10 strings
  required: "number[](1,)",      // At least 1 number
});
Enter fullscreen mode Exit fullscreen mode

Unions and Constants

const schema = Interface({
  status: "pending|approved|rejected",  // Union type
  role: "=admin",                       // Constant value
  priority: "low|medium|high",          // Multiple options
  version: "=1.0.0",                    // Exact match
});
Enter fullscreen mode Exit fullscreen mode

Optional Fields

const schema = Interface({
  id: "number!",          // Required non-zero number
  name: "string!",        // Required non-empty string
  email: "email!?",       // Optional, but if provided must be non-empty
  phone: "string?",       // Optional, can be empty string
  bio: "string!?",        // Optional, but if provided must be non-empty
  tags: "string[]?",      // Optional array
});
Enter fullscreen mode Exit fullscreen mode

πŸ”§ Schema Utilities

Making Fields Optional

const BaseSchema = Interface({
  id: "number",
  name: "string",
  email: "email",
});

// Make specific fields optional
const PartialSchema = Mod.partial(BaseSchema, ['email']);
// Result: { id: number, name: string, email?: string }

// Make all fields optional
const AllOptionalSchema = Mod.partial(BaseSchema);
// Result: { id?: number, name?: string, email?: string }
Enter fullscreen mode Exit fullscreen mode

Omitting Fields

const UserSchema = Interface({
  id: "number",
  name: "string", 
  email: "email",
  password: "string",
});

// Remove sensitive fields
const PublicUserSchema = Mod.omit(UserSchema, ['password']);
// Result: { id: number, name: string, email: string }
Enter fullscreen mode Exit fullscreen mode

Extending Schemas

const BaseSchema = Interface({
  id: "number",
  name: "string",
});

// Add new fields
const ExtendedSchema = Mod.extend(BaseSchema, {
  email: "email",
  createdAt: "date",
});
// Result: { id: number, name: string, email: string, createdAt: Date }
Enter fullscreen mode Exit fullscreen mode

πŸ“– Advanced Examples

API Response Validation

const ApiResponseSchema = Interface({
  success: "boolean",
  data: Interface({
    users: Interface({
      id: "number",
      username: "string(3,20)",
      email: "email",
      role: "admin|user|moderator",
      isActive: "boolean",
      lastLogin: "date?",
    })[],
  }),
  pagination: Interface({
    page: "number(1,)",
    limit: "number(1,100)",
    total: "number(0,)",
  }),
});

// Use in API handler
async function getUsers(req: Request) {
  const response = await fetch('/api/users');
  const rawData = await response.json();

  const result = ApiResponseSchema.safeParse(rawData);

  if (!result.success) {
    throw new Error('Invalid API response format');
  }

  // Fully typed response data
  return result.data;
}
Enter fullscreen mode Exit fullscreen mode

Form Validation

const ContactFormSchema = Interface({
  name: "string!",              // Required non-empty name (basic validation)
  email: "email",               // Required valid email (has built-in validation)
  phone: "string(10,15)?",      // Optional phone, if provided 10-15 chars
  subject: "support|sales|general",
  message: "string(10,1000)",   // Required message, 10-1000 chars (use constraints)
  newsletter: "boolean?",
});

// Test data
const formData = {
  name: "",                     // ❌ Will fail: string! rejects empty
  email: "user@example.com",    // βœ… Valid email
  subject: "support",           // βœ… Valid union value
  message: "Hi there",          // βœ… Valid length
};

function handleFormSubmit(formData: unknown) {
  const result = ContactFormSchema.safeParse(formData);

  if (!result.success) {
    return {
      success: false,
      errors: result.error.issues.map(issue => ({
        field: issue.path.join('.'),
        message: issue.message
      }))
    };
  }

  // Process valid form data
  return { success: true, data: result.data };
}
Enter fullscreen mode Exit fullscreen mode

Configuration Validation

const ConfigSchema = Interface({
  database: Interface({
    host: "string!",            // Required non-empty host
    port: "number(1,65535)",    // Required port with range validation
    name: "string(1,50)",       // Required database name with length constraint
    ssl: "boolean?",
  }),
  redis: Interface({
    url: "url",                 // Required valid URL (has built-in validation)
    ttl: "number(60,)",         // Required TTL with minimum constraint
  })["?"], // Optional nested object
  features: Interface({
    enableCache: "boolean",
    maxUsers: "number!",        // Required non-zero max users
    environment: "development|staging|production",
  }),
});

// Load and validate configuration
function loadConfig(): ConfigType {
  const rawConfig = JSON.parse(process.env.APP_CONFIG || '{}');
  const result = ConfigSchema.safeParse(rawConfig);

  if (!result.success) {
    console.error('Invalid configuration:', result.error.issues);
    process.exit(1);
  }

  return result.data;
}

type ConfigType = typeof ConfigSchema.infer;
Enter fullscreen mode Exit fullscreen mode

🎯 Use Cases

  • API Validation β€” Validate request/response payloads with automatic typing
  • Form Processing β€” Ensure user input meets requirements before processing
  • Configuration Management β€” Validate app settings and environment variables
  • Data Pipelines β€” Type-safe data transformation and validation
  • Database Models β€” Validate data before database operations
  • Integration Testing β€” Ensure external APIs return expected data structures

πŸ› οΈ Error Handling

const result = UserSchema.safeParse(invalidData);

if (!result.success) {
  // Access detailed error information
  result.error.issues.forEach(issue => {
    console.log({
      path: issue.path.join('.'),        // Field path: "user.email"
      code: issue.code,                  // Error code: "invalid_email" 
      message: issue.message,            // Human readable message
      received: issue.received,          // Actual value received
      expected: issue.expected,          // Expected type/format
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

πŸ”— VSCode Extension

Enhance your development experience with our VSCode extension:

  1. Search for "Fortify Schema" in the Extensions marketplace
  2. Install the extension for syntax highlighting and IntelliSense
  3. Enjoy autocomplete and validation in your schema definitions

See the GitHub. Report any bugs to team@nehonix.space

Top comments (0)