Package Exports
- shipping-methods-dsl
- shipping-methods-dsl/package.json
- shipping-methods-dsl/schema
- shipping-methods-dsl/schema/v1
- shipping-methods-dsl/schema/v2
Readme
shipping-methods-dsl
A powerful, type-safe DSL (Domain Specific Language) for defining and evaluating shipping methods across edge workers, frontend, and admin panels.
Features
- Type-safe: Full TypeScript support with comprehensive type definitions
- Validated: Runtime validation using ArkType 2.1
- Flexible pricing: Support for flat, item-based, value-based, tiered, and custom pricing
- Conditional logic: Geo-based and order-based conditions
- Localization: Built-in i18n support for all user-facing strings
- Edge-ready: Works seamlessly in Cloudflare Workers and other edge environments
- Framework agnostic: Use in React, Vue, Svelte, or vanilla JS
- Progressive unlock: Show disabled methods with unlock hints and progress bars
- Zero dependencies: Only requires ArkType for runtime validation
Architecture
This package is designed with tree-shaking and separation of concerns in mind:
Frontend Bundle Backend Bundle
↓ ↓
frontend.ts backend.ts
↓ ↓
DisplayShippingMethod ValidatedShippingMethod
(Complete UI data) (Minimal validation data)Benefits:
- Tree-shaking: Import only what you need - frontend code won't include backend validation logic and vice versa
- Smaller bundles: Frontend gets display logic, backend gets validation logic
- Type safety: Different types for different use cases
- Clear API: Two functions (
getShippingMethodsForDisplayfor FE,getShippingMethodByIdfor BE)
Installation
npm install shipping-methods-dslQuick Start
This package is designed with separation of concerns - different APIs for frontend and backend:
Frontend (Checkout UI)
Get all shipping methods with complete display information for your checkout page.
import {
validateShippingConfig,
getShippingMethodsForDisplay,
type DisplayShippingMethod,
type EvaluationContext,
} from "shipping-methods-dsl";
// 1. Load and validate config (do this once, cache it)
const config = validateShippingConfig(configJson);
// 2. Create context from current cart state
const context: EvaluationContext = {
orderValue: cart.total,
itemCount: cart.items.length,
country: user.country,
locale: user.language,
};
// 3. Get all methods with complete UI information
const methods: DisplayShippingMethod[] = getShippingMethodsForDisplay(config, context);
// 4. Display in your UI
// Note: Methods with availabilityMode: "hide" are automatically filtered out
methods.forEach(method => {
if (method.available) {
// Show as selectable option
console.log(`${method.name} - $${method.price}`);
if (method.badge) console.log(`Badge: ${method.badge}`);
}
// Show upgrade hint (progress bar, etc.)
if (method.nextTier && method.progress) {
console.log(`${method.upgradeMessage}`);
console.log(`Progress: ${method.progress.percentage}%`);
console.log(`Next: ${method.nextTier.label} - $${method.nextTier.price}`);
}
});
// Filter/sort as needed
const available = methods.filter(m => m.available);
const cheapest = available.sort((a, b) => a.price - b.price)[0];Backend (Order Validation)
Validate shipping method selection from frontend during checkout.
import {
validateShippingConfig,
getShippingMethodById,
type ValidatedShippingMethod,
type EvaluationContext,
} from "shipping-methods-dsl";
// 1. Load and validate config
const config = validateShippingConfig(configJson);
// 2. Frontend sends shipping method ID
// POST /api/checkout
// { shippingMethodId: "shipping.us.standard:tier_free", ... }
// 3. Validate the selection
const context: EvaluationContext = {
orderValue: cart.total,
itemCount: cart.items.length,
country: user.country,
};
const method: ValidatedShippingMethod | undefined =
getShippingMethodById(config, req.body.shippingMethodId, context);
if (!method || !method.available) {
return res.status(400).json({ error: "Invalid shipping method" });
}
// 4. Use validated price for final total
const total = cart.total + method.price;
const estimatedDelivery = method.estimatedDays;
// Process order...Configuration
Define your shipping configuration (JSON or TypeScript):
{
"$schema": "https://cdn.jsdelivr.net/npm/shipping-methods-dsl@2/schema/shipping-methods.v2.schema.json",
"version": "1.0",
"currency": "USD",
"methods": [
{
"id": "shipping.us.standard",
"enabled": true,
"name": "Standard Shipping",
"pricing": { "type": "flat", "amount": 5.99 },
"conditions": {
"geo": { "country": { "include": ["US"] } }
}
}
]
}Pricing Types
Flat Rate
Fixed price regardless of order details.
{
"pricing": {
"type": "flat",
"amount": 5.99
}
}Item-Based
Price based on number of items: firstItemPrice + additionalItemPrice × (count - 1)
{
"pricing": {
"type": "item_based",
"firstItemPrice": 12.99,
"additionalItemPrice": 2.5
}
}Value-Based
Percentage of order value with optional min/max clamps.
{
"pricing": {
"type": "value_based",
"percentage": 15,
"minAmount": 15,
"maxAmount": 50
}
}Tiered
Different pricing based on matching criteria (first match wins). Each tier can have either:
- Flat price:
"price": 9.99 - Dynamic pricing:
"pricing": { "type": "item_based", ... }
Example with flat prices:
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "standard",
"criteria": { "order": { "value": { "min": 50, "max": 99.99 } } },
"price": 0,
"estimatedDays": { "min": 7, "max": 10 }
},
{
"id": "express",
"criteria": { "order": { "value": { "min": 100 } } },
"price": 0,
"estimatedDays": { "min": 2, "max": 3 }
}
]
}
}Example with dynamic pricing (item_based):
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "tier_small",
"label": "Express (1-2 items)",
"criteria": { "order": { "items": { "max": 2 } } },
"pricing": {
"type": "item_based",
"firstItemPrice": 10,
"additionalItemPrice": 5
},
"estimatedDays": { "min": 2, "max": 3 }
},
{
"id": "tier_bulk",
"label": "Express (3+ items - bulk discount)",
"criteria": { "order": { "items": { "min": 3 } } },
"pricing": {
"type": "item_based",
"firstItemPrice": 8,
"additionalItemPrice": 3
},
"estimatedDays": { "min": 2, "max": 3 }
}
]
}
}Example with value_based pricing in tiers:
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "tier_standard",
"criteria": { "order": { "value": { "max": 200 } } },
"pricing": {
"type": "value_based",
"percentage": 5,
"minAmount": 10,
"maxAmount": 25
}
},
{
"id": "tier_vip",
"label": "VIP - Free Shipping",
"criteria": { "order": { "value": { "min": 200 } } },
"price": 0
}
]
}
}Seasonal/Holiday Pricing
Use date-based criteria for seasonal pricing (Christmas, Black Friday, etc.).
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "christmas_rush",
"label": "Christmas Express (Order by Dec 20)",
"criteria": {
"date": { "after": "2024-12-10", "before": "2024-12-20" }
},
"price": 14.99,
"estimatedDays": { "min": 2, "max": 3 }
},
{
"id": "post_christmas",
"label": "Express Shipping (After Christmas)",
"criteria": {
"date": { "after": "2024-12-20", "before": "2024-12-27" }
},
"price": 12.99,
"estimatedDays": { "min": 7, "max": 10 }
},
{
"id": "normal",
"label": "Express Shipping",
"criteria": {},
"price": 9.99,
"estimatedDays": { "min": 2, "max": 3 }
}
]
}
}Important: Include orderDate in context to enable date-based matching:
const context = {
orderValue: 100,
itemCount: 2,
country: "US",
orderDate: new Date(), // Required for date-based criteria
};Date Criteria Logic:
Date matching supports full ISO 8601 timestamps with timezone support:
after: Inclusive - timestamp must be ≥ specified timestamp"after": "2024-12-15T14:00:00Z"means orderDate ≥ Dec 15, 2:00 PM UTC"after": "2024-12-15"means orderDate ≥ Dec 15, 12:00 AM (midnight)
before: Exclusive - timestamp must be < specified timestamp"before": "2024-12-20T17:00:00Z"means orderDate < Dec 20, 5:00 PM UTC (excludes exact time)"before": "2024-12-20"means orderDate < Dec 20, 12:00 AM (excludes Dec 20 entirely)
Date-only Examples:
// Date range: Dec 10-19 (inclusive start, exclusive end)
"date": { "after": "2024-12-10", "before": "2024-12-20" }
// Matches: Dec 10 00:00:00 through Dec 19 23:59:59
// Does NOT match: Dec 9, Dec 20
// Only after (no end date)
"date": { "after": "2024-12-31" }
// Matches: Dec 31 00:00:00 and later
// Only before (no start date)
"date": { "before": "2024-12-15" }
// Matches: Any time before Dec 15 00:00:00Time-based Examples:
// Business hours: 9 AM - 5 PM UTC
"date": {
"after": "2024-12-15T09:00:00Z",
"before": "2024-12-15T17:00:00Z"
}
// Matches: 9:00:00 AM through 4:59:59 PM on Dec 15
// Excludes: 5:00:00 PM and later (before is exclusive)
// After 2 PM EST (3 PM UTC during standard time)
"date": { "after": "2024-12-15T15:00:00Z" }
// Matches: Dec 15 3:00 PM UTC and later
// Note: EST is UTC-5, so 2 PM EST = 7 PM UTC (in daylight time)
// With timezone offset notation
"date": { "after": "2024-12-15T14:00:00-05:00" }
// Matches: Dec 15 2:00 PM EST and later (automatically converted to UTC)Timezone Support:
- All ISO 8601 formats supported:
Z(UTC),+HH:MM,-HH:MM - JavaScript Date objects are converted to timestamps preserving timezone
- Comparison is done using milliseconds since epoch (timezone-agnostic)
- Examples:
2024-12-15T14:00:00Z→ Dec 15, 2:00 PM UTC2024-12-15T14:00:00-05:00→ Dec 15, 2:00 PM EST (= 7:00 PM UTC)2024-12-15T14:00:00+09:00→ Dec 15, 2:00 PM JST (= 5:00 AM UTC)
Hide shipping method during specific period:
Use tiered pricing with "OR logic" to show method outside a blackout period:
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "before_holiday",
"criteria": { "date": { "before": "2024-12-15" } },
"price": 29.97
},
{
"id": "after_holiday",
"criteria": { "date": { "after": "2024-12-30" } },
"price": 29.97
}
]
}
}This shows the method before Dec 15 OR after Dec 30, effectively hiding it from Dec 15-30.
Backward compatibility: If orderDate is not provided in context, all date criteria default to true (allow).
Custom
Extensible plugin system for custom logic (e.g., weight-based).
{
"pricing": {
"type": "custom",
"plugin": "weight_based",
"config": {
"ratePerKg": 12000,
"minCharge": 15000
}
}
}Conditions
Geographic Conditions
Filter shipping methods by country and/or state:
{
"conditions": {
"geo": {
"country": {
"include": ["US", "CA"],
"exclude": []
},
"state": {
"include": [],
"exclude": ["AK", "HI", "PR"]
}
}
}
}Country codes: ISO 3166-1 alpha-2 (e.g., "US", "CA", "GB") State codes: ISO 3166-2 (e.g., "US-CA", "CA-ON") or simple codes (e.g., "CA", "AK")
Both include and exclude are optional. If both are provided, include is evaluated first.
State matching: Flexible matching supports both formats:
- Context state "CA" matches config "US-CA" or "CA"
- Context state "US-CA" matches config "CA" or "US-CA"
Example: US Mainland vs Remote States
{
"methods": [
{
"id": "shipping.us.mainland",
"name": "Mainland Shipping",
"conditions": {
"geo": {
"country": { "include": ["US"] },
"state": { "exclude": ["AK", "HI", "PR"] }
}
},
"pricing": { "type": "flat", "amount": 5.99 }
},
{
"id": "shipping.us.remote",
"name": "Alaska/Hawaii Shipping",
"conditions": {
"geo": {
"country": { "include": ["US"] },
"state": { "include": ["AK", "HI"] }
}
},
"pricing": { "type": "flat", "amount": 29.99 }
}
]
}Order Conditions
{
"conditions": {
"order": {
"value": { "min": 50, "max": 500 },
"items": { "min": 1, "max": 10 },
"weight": { "min": 0, "max": 50 }
}
}
}Availability & Upselling
Tier-Level Availability (for Tiered Pricing)
Control how tier upgrade hints appear when users are in a lower tier:
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "tier_paid",
"price": 4.97,
"criteria": { "order": { "value": { "max": 99.99 } } }
},
{
"id": "tier_free",
"price": 0,
"criteria": { "order": { "value": { "min": 100 } } },
"availability": {
"mode": "show_hint",
"when": ["order.value.min"],
"message": "Add ${remaining} more to unlock free shipping",
"showProgress": true
}
}
]
}
}When to use: Show progress hints when user is in paid tier but can upgrade to free tier.
Method-Level Availability (for Non-Tiered Pricing)
Control how methods appear when conditions aren't met:
{
"id": "shipping.promo.free",
"enabled": true,
"name": "Promotional Free Shipping",
"conditions": {
"order": { "value": { "min": 50 } }
},
"pricing": {
"type": "flat",
"amount": 0
},
"availability": {
"mode": "show_hint",
"when": ["order.value.min"],
"message": "Add ${remaining} more to unlock promotional free shipping",
"showProgress": true
}
}When to use: Show hints for flat/item-based/value-based pricing methods that aren't available yet.
Availability Modes
hide- Don't show the method/hint (default) - filtered out bygetShippingMethodsForDisplay()show_disabled- Show as disabled with message (full card UI)show_hint- Show small hint/banner about how to unlock
Key Differences
Tier-level availability:
- Configured on individual tiers (rules)
- Shows hints when user is in a lower tier
- Example: "You're using paid shipping, add $25 more for free"
Method-level availability:
- Configured on the shipping method itself
- Shows hints when method conditions aren't met
- Only for non-tiered pricing (flat, item_based, value_based)
- Example: "Add $20 more to unlock promotional free shipping"
Localization
All user-facing strings support localization:
{
"name": {
"en": "Free Shipping",
"vi": "Miễn phí vận chuyển",
"es": "Envío gratis"
}
}API Reference
Core API (User-Centric Design)
The package provides a minimal, focused API designed around actual use cases:
Frontend (Checkout UI):
getShippingMethodsForDisplay()- Get all methods with complete display information
Backend (Order Validation):
getShippingMethodById()- Validate selection and get pricing
Configuration:
validateShippingConfig()- Validate shipping configuration
Custom Pricing:
registerPricingPlugin()- Register custom pricing logic
Main Functions
validateShippingConfig(data: unknown): ShippingConfig
Validates a shipping configuration against the schema. Throws error if invalid.
import { validateShippingConfig } from "shipping-methods-dsl";
const config = validateShippingConfig(configJson);getShippingMethodsForDisplay(config, context): DisplayShippingMethod[]
Use case: Frontend checkout page - get all shipping methods with complete display information.
Important: Methods with availabilityMode: "hide" are automatically filtered out. Only methods that should be displayed (either available, disabled with hints, or with upgrade progress) are returned.
Returns all visible shipping methods with full display details including:
- Current tier information (name, price, estimatedDays, icon, badge)
- Availability status and display mode
- Upgrade hints with progress tracking
- Next tier information (for tiered pricing)
import { getShippingMethodsForDisplay } from "shipping-methods-dsl";
const methods = getShippingMethodsForDisplay(config, {
orderValue: 75,
itemCount: 2,
country: "US",
locale: "en",
});
// Display in UI
methods.forEach(method => {
if (method.available) {
// Show as selectable option
console.log(`${method.name} - $${method.price}`);
}
// Show upgrade hint
if (method.nextTier && method.progress) {
console.log(`${method.upgradeMessage}`);
console.log(`Next: ${method.nextTier.label} - $${method.nextTier.price}`);
}
});Frontend can filter/sort as needed:
// Get only available methods
const available = methods.filter(m => m.available);
// Get cheapest method
const cheapest = available.sort((a, b) => a.price - b.price)[0];
// Filter by availabilityMode
const hints = methods.filter(m => m.availabilityMode === "show_hint");getShippingMethodById(config, id, context): ValidatedShippingMethod | undefined
Use case: Backend order validation - validate shipping method ID from frontend and get pricing.
Supports both simple IDs ("shipping.express") and tiered IDs ("shipping.express:tier_premium").
import { getShippingMethodById } from "shipping-methods-dsl";
// Frontend sends: { shippingMethodId: "shipping.us.standard:tier_free" }
// Backend validates
const method = getShippingMethodById(config, shippingMethodId, {
orderValue: cart.total,
itemCount: cart.items.length,
country: user.country,
});
if (!method || !method.available) {
throw new Error("Invalid shipping method");
}
// Use validated price for final calculation
const total = cart.total + method.price;registerPricingPlugin(name, handler)
Register custom pricing logic for advanced use cases.
import { registerPricingPlugin } from "shipping-methods-dsl";
registerPricingPlugin("weight_based", (config, context) => {
const ratePerKg = config.ratePerKg as number;
const weight = context.weight || 0;
return weight * ratePerKg;
});Usage Examples
Backend: Validate Shipping Method from Frontend
When your frontend sends a selected shipping method ID during checkout, use getShippingMethodById to validate and calculate the actual price:
import {
type ShippingConfig,
getShippingMethodById,
validateShippingConfig,
} from "shipping-methods-dsl";
// Load your config (from KV, R2, or environment)
const config = validateShippingConfig(configJson);
export default {
async fetch(request: Request): Promise<Response> {
const body = await request.json();
const { shippingMethodId, orderValue, itemCount, country } = body;
// Create context from order data
const context = {
orderValue,
itemCount,
country,
locale: request.headers.get("Accept-Language") || "en",
};
// Validate the shipping method ID from frontend
const shippingMethod = getShippingMethodById(config, shippingMethodId, context);
if (!shippingMethod) {
return Response.json({ error: "Invalid shipping method" }, { status: 400 });
}
if (!shippingMethod.available) {
return Response.json({
error: "Shipping method not available",
message: shippingMethod.message
}, { status: 400 });
}
// Use the validated shipping price
const shippingCost = shippingMethod.price;
const total = orderValue + shippingCost;
return Response.json({
shippingCost,
shippingMethod: {
id: shippingMethod.id,
name: shippingMethod.name,
estimatedDays: shippingMethod.estimatedDays,
},
total,
});
}
};See examples/ for more detailed examples including:
- Full Cloudflare Worker implementation
- Backend validation example
- React frontend component
- Custom plugin usage
Cloudflare Worker Compatibility
This package is 100% compatible with Cloudflare Workers and other edge runtimes:
- ✅ Zero Node.js dependencies
- ✅ Pure JavaScript/TypeScript
- ✅ ES Modules only
- ✅ Bundle size: ~115KB
- ✅ Works on: Workers, Node.js 18+, Deno, Browsers, Bun
See CLOUDFLARE_WORKER_COMPATIBILITY.md for details.
JSON Schema
Use the JSON Schema for IDE autocomplete and validation:
{
"$schema": "https://cdn.jsdelivr.net/npm/shipping-methods-dsl@2/schema/shipping-methods.v2.schema.json"
}Processing Time
Configure global processing time that varies by season (e.g., Christmas rush, Black Friday).
Basic Usage
import { getProcessingTime } from "shipping-methods-dsl";
// Get current processing time
const processingTime = getProcessingTime(config);
console.log(`Processing: ${processingTime.min}-${processingTime.max} days`);
// Get processing time for specific date
const christmasTime = getProcessingTime(config, new Date("2024-12-15"));
console.log(`Christmas processing: ${christmasTime.min}-${christmasTime.max} days`);Configuration
{
"version": "1.0",
"processingTime": {
"default": { "min": 1, "max": 2 },
"seasonal": [
{
"after": "2024-11-20T00:00:00Z",
"before": "2024-11-30T00:00:00Z",
"days": { "min": 2, "max": 3 }
},
{
"after": "2024-12-01T00:00:00Z",
"before": "2024-12-26T00:00:00Z",
"days": { "min": 3, "max": 5 }
}
]
},
"methods": [...]
}Frontend Display
const methods = getShippingMethodsForDisplay(config, context);
const processingTime = getProcessingTime(config, context.orderDate);
methods.forEach(method => {
const shipping = method.estimatedDays;
const total = {
min: processingTime.min + (shipping?.min || 0),
max: processingTime.max + (shipping?.max || 0)
};
console.log(`${method.name}`);
console.log(`Processing: ${processingTime.min}-${processingTime.max} days`);
console.log(`Shipping: ${shipping?.min}-${shipping?.max} days`);
console.log(`Total delivery: ${total.min}-${total.max} days`);
});Features:
- Global processing time (applies to all methods)
- Seasonal overrides with date ranges
- Multiple seasonal periods supported
- Automatic fallback to default when not in seasonal period
- Date range:
afteris inclusive (>=),beforeis exclusive (<)
Type Definitions
Full TypeScript type definitions are included:
import type {
ShippingConfig,
ShippingMethod,
Pricing,
EvaluationContext,
ShippingCalculationResult,
} from "shipping-methods-dsl";Development
# Install dependencies
npm install
# Build
npm run build
# Type check
npm run type-check
# Watch mode
npm run devContributing
Contributions are welcome! Please ensure:
- All code passes type checking
- Follow existing patterns and conventions
- Add tests for new features
License
MIT - See LICENSE
Advanced Features
Estimated Delivery Days
Configure estimated delivery timeframes for shipping methods:
{
"id": "shipping.us.express",
"name": "Express Shipping",
"pricing": { "type": "flat", "amount": 9.99 },
"estimatedDays": { "min": 2, "max": 3 }
}For tiered pricing, each tier can have its own delivery estimate:
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "tier_standard",
"price": 0,
"estimatedDays": { "min": 5, "max": 7 }
},
{
"id": "tier_express",
"price": 0,
"estimatedDays": { "min": 2, "max": 3 }
}
]
}
}Display Configuration
Control how shipping methods appear in your UI:
{
"display": {
"priority": 1,
"badge": "Popular",
"promoText": {
"en": "Save $5 on orders over $50",
"vi": "Tiết kiệm $5 cho đơn hàng trên $50"
}
}
}Fields:
priority: Sort order (lower numbers appear first)badge: Display badge like "Popular", "Fastest", etc.promoText: Promotional message (localized)
Progress Tracking
Show users how close they are to unlocking better shipping options.
For tiered pricing: Add availability config to the target tier:
{
"pricing": {
"type": "tiered",
"rules": [
{
"id": "tier_paid",
"price": 4.97,
"criteria": { "order": { "value": { "max": 99.99 } } }
},
{
"id": "tier_free",
"price": 0,
"criteria": { "order": { "value": { "min": 100 } } },
"availability": {
"mode": "show_hint",
"when": ["order.value.min"],
"message": "Add ${remaining} more to unlock free shipping",
"showProgress": true
}
}
]
}
}Access progress information in your UI:
For non-tiered pricing: Add availability config to the method:
{
"id": "shipping.promo.free",
"name": "Promotional Free Shipping",
"conditions": {
"order": { "value": { "min": 50 } }
},
"pricing": { "type": "flat", "amount": 0 },
"availability": {
"mode": "show_hint",
"when": ["order.value.min"],
"message": "Add ${remaining} more to unlock promotional free shipping",
"showProgress": true
}
}Access progress information in your UI:
const methods = getShippingMethodsForDisplay(config, context);
methods.forEach(method => {
// Progress is shown when:
// - Tiered: user is in lower tier, better tier has availability config
// - Non-tiered: method conditions not met, has availability config
if (method.progress && method.nextTier) {
const { current, required, remaining, percentage } = method.progress;
const { label, price, estimatedDays } = method.nextTier;
console.log(`Current: ${method.name} - $${method.price}`);
console.log(`Progress: ${percentage.toFixed(0)}%`);
console.log(`Next tier: ${label} - $${price}`);
if (estimatedDays) {
console.log(`Delivery: ${estimatedDays.min}-${estimatedDays.max} days`);
}
console.log(`${method.upgradeMessage}`); // e.g., "Add $25 more to unlock free shipping"
}
});Example output:
Current: Standard Shipping - $4.97
Progress: 75%
Next tier: Free Standard Shipping - $0
Delivery: 5-7 days
Add $25.00 more to unlock free shippingMetadata
Attach custom metadata to shipping methods:
{
"id": "shipping.us.express",
"name": "Express Shipping",
"meta": {
"carrier": "FedEx",
"serviceCode": "FEDEX_2_DAY",
"trackingEnabled": true,
"insurance": true
}
}Access metadata in your application:
const method = getShippingMethodById(config, "shipping.us.express", context);
console.log(method?.meta?.carrier); // "FedEx"Complete Configuration Example
See examples/us-international-shipping.json for a complete real-world configuration including:
- US Standard Shipping (tiered pricing with 2 tiers):
- Paid: $4.97 (orders < $100, 5-7 days)
- Free: $0 (orders ≥ $100, 5-7 days)
- Progress tracking: "Add $X more to unlock free standard shipping"
- US Express Shipping (tiered pricing with 2 tiers):
- Paid: $9.97 (orders < $500, 2-3 days)
- Free: $0 (orders ≥ $500, 2-3 days)
- Progress tracking: "Add $X more to unlock free express shipping"
- US Overnight Shipping: Item-based pricing ($29.97 + $10.97/item, next day)
- International Standard: Item-based pricing ($9.97 + $3.97/item, 10-15 days)
- Localized strings (English & Vietnamese)
- Display priorities and badges
TypeScript Types Reference
Core Types
// Configuration context for evaluation
interface EvaluationContext {
orderValue: number;
itemCount: number;
weight?: number;
country: string; // ISO 3166-1 alpha-2 (e.g., "US", "CA")
state?: string; // ISO 3166-2 (e.g., "US-CA") or simple code (e.g., "CA", "AK")
currency?: string; // ISO 4217 (e.g., "USD")
locale?: string; // Language code (e.g., "en", "vi")
orderDate?: Date; // Order date for seasonal/holiday pricing
}
// Localized string (single string or locale map)
type LocalizedString = string | Record<string, string>;
// Numeric range
interface RangeNumber {
min?: number;
max?: number;
}
// Estimated delivery days
interface EstimatedDays {
min: number;
max: number;
}
// Geographic conditions
interface GeoCountry {
include?: string[]; // Country codes to include (ISO 3166-1 alpha-2)
exclude?: string[]; // Country codes to exclude
}
interface GeoState {
include?: string[]; // State codes to include (ISO 3166-2 or simple codes)
exclude?: string[]; // State codes to exclude
}
// Order-based conditions
interface OrderConditions {
value?: RangeNumber; // Order value range
items?: RangeNumber; // Item count range
weight?: RangeNumber; // Weight range
}
// Date-based conditions for seasonal/holiday pricing
interface DateCriteria {
after?: string; // ISO 8601 date string - inclusive (orderDate >= after)
before?: string; // ISO 8601 date string - exclusive (orderDate < before)
}
// Pricing types
type Pricing =
| { type: "flat"; amount: number }
| { type: "item_based"; firstItemPrice: number; additionalItemPrice: number }
| { type: "value_based"; percentage: number; minAmount?: number; maxAmount?: number }
| { type: "tiered"; rules: Rule[] }
| { type: "custom"; plugin: string; config: Record<string, unknown> };
// Tiered pricing rule
interface Rule {
id: string;
label?: LocalizedString;
criteria: {
geo?: { country?: GeoCountry };
order?: OrderConditions;
date?: DateCriteria; // Date-based criteria for seasonal pricing
};
price: number;
estimatedDays?: EstimatedDays;
promoText?: LocalizedString;
upgradeMessage?: LocalizedString;
availability?: Availability; // Tier-level availability for upgrade hints
}
// Availability configuration
interface Availability {
mode: "hide" | "show_disabled" | "show_hint";
when?: Array<"order.value.min" | "order.items.min" | "order.weight.min">;
message?: LocalizedString;
showProgress?: boolean;
}
// Display configuration
interface Display {
priority?: number;
badge?: string;
promoText?: LocalizedString;
}
// Shipping method definition
interface ShippingMethod {
id: string;
enabled: boolean;
name: LocalizedString;
description?: LocalizedString;
icon?: string;
display?: Display;
conditions?: {
geo?: { country?: GeoCountry };
order?: OrderConditions;
date?: DateCriteria; // Date-based criteria (method-level)
};
pricing: Pricing;
availability?: Availability; // Method-level availability for non-tiered pricing
estimatedDays?: EstimatedDays;
meta?: Record<string, unknown>;
}
// Root configuration
interface ShippingConfig {
$schema?: string;
version: "1.0";
currency?: string;
methods: ShippingMethod[];
}
// ============================================
// FRONTEND TYPE - For UI display
// ============================================
interface DisplayShippingMethod {
// Identity
id: string; // Full ID: "method_id" or "method_id:tier_id"
methodId: string;
tierId?: string;
// Display Information
name: string; // Localized
description?: string; // Localized
icon?: string;
badge?: string;
// Pricing & Availability
price: number;
available: boolean;
enabled: boolean;
estimatedDays?: EstimatedDays;
// Availability Mode (how to display in UI)
availabilityMode?: "hide" | "show_disabled" | "show_hint";
message?: string;
promoText?: string; // Localized
upgradeMessage?: string; // Localized
// Progress Tracking
progress?: {
current: number;
required: number;
remaining: number;
percentage: number;
};
// Next Tier Information (for upgrade hints)
nextTier?: {
id: string;
label?: string; // Localized
price: number;
estimatedDays?: EstimatedDays;
};
// Custom Metadata
meta?: Record<string, unknown>;
}
// ============================================
// BACKEND TYPE - For order validation
// ============================================
interface ValidatedShippingMethod {
// Identity
id: string; // Full ID that was validated
methodId: string;
tierId?: string;
// Validation Result
available: boolean;
enabled: boolean;
// Pricing (what matters for checkout)
price: number;
estimatedDays?: EstimatedDays;
// Display info (for order confirmation)
name: string; // Localized
description?: string; // Localized
// Custom Metadata
meta?: Record<string, unknown>;
}
// Custom pricing plugin function
type CustomPricingPlugin = (
config: Record<string, unknown>,
context: EvaluationContext
) => number;API Functions
Validation
function validateShippingConfig(data: unknown): ShippingConfigValidates a shipping configuration against the schema. Throws an error if validation fails.
Example:
try {
const config = validateShippingConfig(configJson);
console.log("Config is valid");
} catch (error) {
console.error("Invalid config:", error.message);
}Plugin System
function registerPricingPlugin(
name: string,
handler: CustomPricingPlugin
): voidRegister a custom pricing plugin.
Example:
import { registerPricingPlugin } from "shipping-methods-dsl";
registerPricingPlugin("weight_based", (config, context) => {
const ratePerKg = config.ratePerKg as number;
const minCharge = config.minCharge as number;
const weight = context.weight || 0;
return Math.max(weight * ratePerKg, minCharge);
});function getPricingPlugin(name: string): CustomPricingPlugin | undefinedGet a registered pricing plugin by name.
Condition Evaluation
function evaluateConditions(
conditions: Conditions | undefined,
context: EvaluationContext
): booleanEvaluate if conditions are met for the given context.
function calculateRemaining(
condition: string,
conditions: Conditions | undefined,
context: EvaluationContext
): numberCalculate how much is remaining to meet a specific condition.
function getMinimumRequired(
condition: string,
conditions: Conditions | undefined
): number | undefinedGet the minimum value required for a specific condition.
Best Practices
1. Configuration Management
Store your shipping configuration in a centralized location:
- Cloudflare Workers: Use KV or R2 storage
- Node.js: Use environment variables or config files
- Frontend: Fetch from API endpoint
Always validate configuration at load time:
import { validateShippingConfig } from "shipping-methods-dsl";
// In your app initialization
const config = validateShippingConfig(await loadConfig());2. Caching
Cache the validated configuration to avoid repeated validation:
let cachedConfig: ShippingConfig | null = null;
export function getConfig(): ShippingConfig {
if (!cachedConfig) {
cachedConfig = validateShippingConfig(rawConfig);
}
return cachedConfig;
}3. Backend Validation
Always validate shipping selection on the backend, even if you validate on the frontend:
// Frontend sends: { shippingMethodId: "shipping.express:tier_premium", ... }
// Backend validates:
const method = getShippingMethodById(config, shippingMethodId, context);
if (!method || !method.available) {
throw new Error("Invalid shipping method");
}
// Use method.price for final calculation
const total = orderValue + method.price;4. Error Handling
Always handle cases where no shipping methods are available:
const methods = getAvailableShippingMethods(config, context);
if (methods.length === 0) {
// Show message: "No shipping methods available for your location"
// Or suggest changes: "Reduce order weight" or "Change delivery country"
}5. Localization
Provide localized strings for all user-facing text:
{
"name": {
"en": "Free Shipping",
"vi": "Miễn phí vận chuyển",
"es": "Envío gratis",
"fr": "Livraison gratuite"
}
}Always pass the user's locale in the context:
const context: EvaluationContext = {
orderValue: cart.total,
itemCount: cart.items.length,
country: user.country,
locale: user.preferredLanguage || "en",
};6. Testing
Test your shipping configuration with various contexts:
// Test different countries
const usContext = { ...baseContext, country: "US" };
const caContext = { ...baseContext, country: "CA" };
// Test different order values
const smallOrder = { ...baseContext, orderValue: 25 };
const largeOrder = { ...baseContext, orderValue: 500 };
// Test item counts
const singleItem = { ...baseContext, itemCount: 1 };
const bulkOrder = { ...baseContext, itemCount: 50 };Troubleshooting
Configuration Validation Errors
If you get validation errors, check:
- Required fields: All methods must have
id,enabled,name, andpricing - Version: Must be exactly
"1.0" - Country codes: Must be valid ISO 3166-1 alpha-2 codes
- Pricing rules: Each tiered rule must have
id,criteria, andprice
No Shipping Methods Available
If no methods are returned:
- Check geo conditions: Does the user's country match
includelist? - Check order conditions: Does order value/items/weight meet requirements?
- Check enabled flag: Are methods enabled in config?
- Use getShippingMethodsForDisplay: See all methods with their availability status
Tiered Pricing Not Working
Common issues:
- Base conditions not met: Tiered pricing checks base conditions first, then tier criteria
- Tier order matters: First matching tier wins (order matters!)
- No matching tier: Make sure at least one tier's criteria matches the context
TypeScript Errors
Make sure you're importing types correctly:
Frontend:
import type {
ShippingConfig,
EvaluationContext,
DisplayShippingMethod,
} from "shipping-methods-dsl";Backend:
import type {
ShippingConfig,
EvaluationContext,
ValidatedShippingMethod,
} from "shipping-methods-dsl";Links
Support
For issues, questions, or contributions, please visit the GitHub repository.