Builder API
The Builder API is the core of Luq's validation system. It provides a fluent, type-safe interface for constructing validators with plugins and field definitions.
Overview
The Builder follows a fluent API pattern with these key steps:
- Create - Initialize a new Builder instance
- Configure - Add plugins with
use()
- Type - Set the validation target type with
for<T>()
- Define - Add field validations with
v()
- Build - Create the final validator with
build()
Creating a Builder
import { Builder } from '@maroonedog/luq/core';
// Create a new builder instance
const builder = Builder();
The Builder()
function creates a new builder instance. It's a factory function that returns a chainable builder object.
use() - Adding Plugins
The use()
method adds plugins to the builder. Plugins provide validation methods that become available on field builders.
import {
requiredPlugin,
stringMinPlugin,
numberMinPlugin,
stringEmailPlugin
} from '@maroonedog/luq/plugin';
// Add plugins one by one
const builder = Builder()
.use(requiredPlugin)
.use(stringMinPlugin)
.use(numberMinPlugin)
.use(stringEmailPlugin);
// Or add multiple plugins at once
const builder = Builder()
.use(
requiredPlugin,
stringMinPlugin,
numberMinPlugin,
stringEmailPlugin
);
💡 Plugin Loading
- • Plugins must be added before calling
for()
- • Each plugin is loaded only once (duplicates are ignored)
- • Plugins determine available validation methods
- • Order doesn't matter for most plugins
for() - Setting Type
The for<T>()
method sets the TypeScript type that the validator will validate. This enables type-safe field paths and type inference.
type UserProfile = {
name: string;
age: number;
email: string;
preferences?: {
theme: 'light' | 'dark';
notifications: boolean;
};
};
// Specify the type your validator will validate
const builder = Builder()
.use(requiredPlugin)
.use(stringMinPlugin)
.for<UserProfile>(); // Type parameter sets the validation target
Field Definition Methods
v() - Define Field
The v()
method (short for "validate") defines validation rules for a field. It takes a field path and a builder function.
// Basic field definition
builder.v('name', b => b.string.required().min(2));
// Nested field using dot notation
builder.v('preferences.theme', b => b.string.oneOf(['light', 'dark']));
// Array element validation
builder.v('tags[*]', b => b.string.required().min(1));
// Complex validation chain
builder.v('email', b =>
b.string
.required()
.email()
.transform(email => email.toLowerCase())
);
📝 Field Path Syntax
- •
fieldName
- Top-level field - •
nested.field.path
- Nested object fields - •
array[*]
- All array elements - •
nested.array[*].field
- Fields in array objects
Field Options
The v()
method accepts an optional third parameter for field-specific configuration:
interface FieldOptions<T> {
default?: T | (() => T); // Default value when undefined/null
applyDefaultToNull?: boolean; // Apply default to null (default: true)
description?: string; // Field description for documentation
deprecated?: boolean | string; // Mark field as deprecated
metadata?: Record<string, any>; // Custom metadata
}
// Using field options with default value
builder.v('theme',
b => b.string.optional(),
{
default: 'light',
description: 'User theme preference',
metadata: { group: 'preferences' }
}
);
// Shorthand syntax - just default value
builder.v('language',
b => b.string.optional(),
'en' // Default value shorthand
);
// Function as default value
builder.v('timestamp',
b => b.number.optional(),
() => Date.now() // Dynamic default
);
// With deprecated field
builder.v('oldField',
b => b.string.optional(),
{
deprecated: 'Use newField instead',
default: ''
}
);
useField() - Reuse Rules
The useField()
method allows you to reuse pre-defined field rules across multiple fields. Field rules are created using the Plugin Registry's createFieldRule()
method:
import { createPluginRegistry } from '@maroonedog/luq/core/registry';
import {
requiredPlugin,
stringEmailPlugin,
stringMinPlugin
} from '@maroonedog/luq/plugin';
// Create registry with plugins
const registry = createPluginRegistry()
.register(requiredPlugin)
.register(stringEmailPlugin)
.register(stringMinPlugin);
// Create reusable field rules using registry
const emailRule = registry.createFieldRule<string>(
b => b.string.required().email(),
{
name: 'email',
description: 'Valid email address',
fieldOptions: {
default: '',
metadata: { validation: 'strict' }
}
}
);
const passwordRule = registry.createFieldRule<string>(
b => b.string.required().min(8),
{ name: 'password', description: 'Secure password' }
);
// Use field rules in builder
const builder = registry
.toBuilder()
.for<{ email: string; password: string; backupEmail: string }>()
.useField('email', emailRule)
.useField('backupEmail', emailRule)
.useField('password', passwordRule);
📌 About FieldRule
- • FieldRule objects are created through
registry.createFieldRule()
- • They encapsulate validation logic and field options
- • Each rule can be tested individually with
rule.validate(value)
- • Rules are reusable across multiple fields and validators
- • See the Plugin Registry API for details
Builder Modes
strict() Mode
The strict()
method enables compile-time type checking to ensure all fields in the type have validation rules defined. When fields are missing, it shows errors in your editor/IDE and prevents compilation:
type User = {
id: string;
name: string;
email: string;
age: number;
};
// Without strict mode - no type checking for missing fields
const validator = Builder()
.use(requiredPlugin)
.for<User>()
.v('name', b => b.string.required())
.v('email', b => b.string.required())
.build(); // Missing 'id' and 'age' - builds successfully
// With strict mode - type error if fields are missing
const strictValidator = Builder()
.use(requiredPlugin)
.for<User>()
.v('id', b => b.string.required())
.v('name', b => b.string.required())
.v('email', b => b.string.required())
.v('age', b => b.number.required())
.strict() // ✅ All fields defined - returns builder
.build();
// Attempting strict with missing fields
const incompleteValidator = Builder()
.use(requiredPlugin)
.for<User>()
.v('name', b => b.string.required())
.strict() // ❌ Type error: Returns error object instead of builder
.build(); // TypeScript error: Property 'build' does not exist on type
// Can continue adding validations after strict()
const continueAfterStrict = Builder()
.use(requiredPlugin, stringMinPlugin)
.for<User>()
.v('id', b => b.string.required())
.v('name', b => b.string.required())
.v('email', b => b.string.required())
.v('age', b => b.number.required())
.strict() // All fields present - returns builder
.v('name', b => b.string.required().min(2)) // Can add more validations
.build(); // Works fine
💡 Strict Mode Behavior
- • Editor Warning: Shows type errors in your editor/IDE when fields are missing
- • Compile-Time Check: Prevents TypeScript compilation if not all fields have validators
- • Return Type: Uses conditional types - returns error type if fields missing, builder if complete
- • Continuable: If all fields are present, you can continue chaining more validations after
strict()
- • Position Flexible: Can be called at any point in the chain, not just at the end
- • NO Runtime Effect: Does NOT validate for extra/undefined fields at runtime
- • Error Prevention: Error type lacks
build()
method, preventing incomplete validators from being built
📝 Note
There's also a strictOnEditor()
method which is just an alias for strict()
. They are identical - use strict()
as it's more concise.
⚠️ Common Misconception
strict()
does NOT validate for extra properties at runtime. It only ensures all type fields have validators at compile time.
To reject extra properties at runtime, use the objectAdditionalProperties
plugin with additionalProperties(false)
:
// To reject extra properties at runtime, use objectAdditionalProperties
import { objectAdditionalPropertiesPlugin } from '@maroonedog/luq/plugin';
type User = {
name: string;
age: number;
email: string;
};
const validator = Builder()
.use(requiredPlugin)
.use(objectAdditionalPropertiesPlugin)
.for<{ user: User }>()
.v('user', b => b.object.additionalProperties(false, {
allowedProperties: ['name', 'age', 'email']
}))
.build();
// This will fail validation
const result = validator.validate({
user: {
name: 'John',
age: 30,
email: 'john@example.com',
extra: 'field' // ❌ Extra property not allowed
}
});
build() - Create Validator
The build()
method finalizes the builder and returns a validator instance:
// build() creates the final validator instance
const validator = Builder()
.use(requiredPlugin)
.use(stringMinPlugin)
.for<UserProfile>()
.v('name', b => b.string.required().min(2))
.v('age', b => b.number.required().min(18))
.build();
// The validator has two main methods:
// 1. validate() - validates and returns original values
const validateResult = validator.validate({
name: 'John',
age: 25
});
// 2. parse() - validates and returns transformed values
const parseResult = validator.parse({
name: 'John',
age: 25
});
⚠️ validate() vs parse()
- •
validate()
- Returns original values (no transformations) - •
parse()
- Returns transformed values (applies all transformations) - • Use
parse()
when you have transform plugins
Type Inference
Method Chaining
The Builder uses advanced TypeScript features to provide complete type safety and inference throughout the chain:
type FormData = {
username: string;
age: string; // String from form input
tags: string; // Comma-separated string
};
type ProcessedData = {
username: string; // Stays string
age: number; // Transformed to number
tags: string[]; // Transformed to array
};
const validator = Builder()
.use(requiredPlugin)
.use(transformPlugin)
.for<FormData>()
.v('username', b => b.string.required())
.v('age', b => b.string.required().transform(v => parseInt(v, 10)))
.v('tags', b => b.string.required().transform(v => v.split(',')))
.build();
// TypeScript knows the types
const result = validator.parse({
username: 'john',
age: '25',
tags: 'js,ts,react'
});
if (result.isValid()) {
const data = result.unwrap();
// data.age is number
// data.tags is string[]
}
Complete Example
Here's a comprehensive example showing all Builder API features:
import { Builder } from '@maroonedog/luq/core';
import {
requiredPlugin,
optionalPlugin,
stringMinPlugin,
stringMaxPlugin,
stringEmailPlugin,
stringPatternPlugin,
numberMinPlugin,
numberMaxPlugin,
arrayMinLengthPlugin,
arrayMaxLengthPlugin,
objectPlugin,
oneOfPlugin,
compareFieldPlugin,
transformPlugin
} from '@maroonedog/luq/plugin';
// Define your data types
type RegistrationForm = {
username: string;
email: string;
password: string;
confirmPassword: string;
age: string; // From form input
acceptTerms: boolean;
preferences?: {
newsletter: boolean;
theme: 'light' | 'dark' | 'auto';
};
interests: string[];
};
// Build comprehensive validator
const registrationValidator = Builder()
// Add all required plugins
.use(
requiredPlugin,
optionalPlugin,
stringMinPlugin,
stringMaxPlugin,
stringEmailPlugin,
stringPatternPlugin,
numberMinPlugin,
arrayMinLengthPlugin,
arrayMaxLengthPlugin,
objectPlugin,
oneOfPlugin,
compareFieldPlugin,
transformPlugin
)
// Set the type
.for<RegistrationForm>()
// Define field validations
.v('username', b =>
b.string
.required()
.min(3)
.max(20)
.pattern(/^[a-zA-Z0-9_]+$/)
)
.v('email', b =>
b.string
.required()
.email()
.transform(email => email.toLowerCase())
)
.v('password', b =>
b.string
.required()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
)
.v('confirmPassword', b =>
b.string
.required()
.compareField('password')
)
.v('age', b =>
b.string
.required()
.transform(v => parseInt(v, 10))
.min(13)
.max(120)
)
.v('acceptTerms', b =>
b.boolean
.required()
.equals(true)
)
.v('preferences', b =>
b.object.optional()
)
.v('preferences.newsletter', b =>
b.boolean.optional()
)
.v('preferences.theme', b =>
b.string
.optional()
.oneOf(['light', 'dark', 'auto'])
)
.v('interests', b =>
b.array
.required()
.minLength(1)
.maxLength(10)
)
.v('interests[*]', b =>
b.string.required().min(2)
)
// Ensure all required fields are defined
.strict()
// Build the final validator
.build();
// Use the validator
const formData = {
username: 'john_doe',
email: 'JOHN@EXAMPLE.COM',
password: 'SecurePass123',
confirmPassword: 'SecurePass123',
age: '25',
acceptTerms: true,
preferences: {
newsletter: true,
theme: 'dark' as const
},
interests: ['programming', 'music', 'sports']
};
// Validate and transform
const result = registrationValidator.parse(formData);
if (result.isValid()) {
const validatedData = result.unwrap();
console.log('Registration successful!', validatedData);
// validatedData.email is 'john@example.com' (lowercased)
// validatedData.age is 25 (number)
} else {
console.log('Validation errors:', result.errors);
}
API Reference
Method | Returns | Description |
---|---|---|
Builder() | IChainableBuilder | Creates new builder instance |
use(...plugins) | IChainableBuilder | Adds plugins to builder |
for<T>() | FieldBuilder | Sets validation target type |
v(path, builder, options?) | FieldBuilder | Defines field validation |
useField(path, rule) | FieldBuilder | Uses pre-defined rule |
strict() | FieldBuilder | Error | Type-check all fields defined |
build() | Validator | Creates final validator |