Why Schemas?

Schemas are the foundation of Routier’s data management system. This document explains why schemas are important and how they benefit your application.

Quick Navigation

What Are Schemas?

A schema is a blueprint that defines:

  • Structure - What properties your data has
  • Types - What kind of values each property can hold
  • Constraints - Rules that data must follow
  • Behavior - How properties should behave (computed, tracked, etc.)
  • Metadata - Information for indexing, relationships, and more

Benefits of Using Schemas

1. Type Safety

Schemas provide compile-time type checking and type safety:

import { s, InferType } from "@routier/core/schema";

// Type safety example - schemas provide compile-time type checking
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
}).compile();

// TypeScript automatically infers the correct types
type User = InferType<typeof userSchema>;
// User = { id: string; name: string; email: string; age: number; }

// Type-safe function parameters
function processUser(user: User): string {
    return `${user.name} (${user.email})`;
}

// Compile-time type checking prevents errors
// processUser({ name: "John" }); // ❌ TypeScript error - missing required fields
// processUser({ name: "John", email: "[email protected]", age: "thirty" }); // ❌ TypeScript error - wrong type

2. Type Safety and Constraints

Schemas ensure data structure matches your defined types, reducing bugs and improving data quality:

import { s } from "@routier/core/schema";

// Type safety and constraints example
const orderSchema = s.define("orders", {
    id: s.string().key().identity(),
    status: s.string("pending", "processing", "shipped", "delivered"), // Constrained to specific values
    priority: s.number(1, 2, 3, 4, 5).default(1), // Constrained to specific numbers
    customerId: s.string().distinct(), // Ensures uniqueness
    createdAt: s.date().default(() => new Date()),
}).compile();

// Schemas ensure data structure matches your defined types
// This prevents bugs and improves data quality:

// ✅ Valid data structure
const validOrder = {
    status: "pending", // Must be one of the allowed values
    priority: 3, // Must be 1-5
    customerId: "customer-123",
    // createdAt will be auto-generated
};

// ❌ Invalid data (TypeScript will catch these)
// const invalidOrder = {
//   status: "invalid", // Not in allowed values
//   priority: 10, // Not in allowed range
//   customerId: "customer-123", // Could be duplicate
// };

3. Self-Documenting Code

Schemas serve as living documentation of your data structures:

import { s } from "@routier/core/schema";

// Self-documenting code example
const productSchema = s.define("products", {
    // Clear structure - anyone can understand what a product looks like
    id: s.string().key().identity(),
    name: s.string(),
    description: s.string().optional(),
    price: s.number(),
    category: s.string("electronics", "books", "clothing", "home"),
    inStock: s.boolean().default(true),

    // Metadata that explains business rules
    createdAt: s.date().default(() => new Date()).readonly(),
    updatedAt: s.date().default(() => new Date()),

    // Constraints that document business requirements
    sku: s.string().distinct(), // Must be unique
    tags: s.array(s.string()).default([]),
}).compile();

// The schema serves as living documentation:
// - Shows what properties a product has
// - Documents business rules (unique SKU, required name)
// - Shows data types and constraints
// - Indicates which fields are optional vs required
// - Documents default values and behavior

4. Automatic Features

Schemas enable powerful features without additional code:

import { s } from "@routier/core/schema";

// Automatic features example
const blogPostSchema = s.define("blogPosts", {
    id: s.string().key().identity(),
    title: s.string(),
    content: s.string(),
    author: s.string(),
    publishedAt: s.date().optional(),
}).modify(w => ({
    // Computed properties - automatically calculated
    slug: w.computed((entity) =>
        entity.title.toLowerCase().replace(/\s+/g, '-')
    ).tracked(),

    // Function properties - non-persisted methods
    isPublished: w.function((entity) => entity.publishedAt != null),
})).compile();

// Schemas enable powerful features without additional code:
// - Automatic ID generation (identity)
// - Computed properties (slug from title)
// - Indexing for fast queries (distinct, index)
// - Change tracking and persistence
// - Serialization/deserialization
// - Type inference and safety

5. Consistent Data Handling

Schemas ensure all parts of your application handle data the same way:

import { s } from "@routier/core/schema";

// Consistent data handling example
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    email: s.string().distinct(),
    profile: s.object({
        firstName: s.string(),
        lastName: s.string(),
        avatar: s.string().optional(),
    }),
    preferences: s.object({
        theme: s.string("light", "dark").default("light"),
        notifications: s.boolean().default(true),
    }).default({}),
}).compile();

// Schemas ensure all parts of your application handle data the same way:
// - Same structure across frontend, backend, and database
// - Consistent serialization/deserialization
// - Uniform validation and type checking
// - Same default values everywhere
// - Consistent indexing and querying
// - Same computed properties and functions

// Whether you're:
// - Creating a user in the frontend
// - Processing data in the backend  
// - Storing data in the database
// - Syncing between systems
// The schema ensures consistent behavior everywhere

6. Performance Optimization

Schemas enable automatic performance optimizations:

import { s } from "@routier/core/schema";

// Performance optimization example
const analyticsSchema = s.define("analytics", {
    id: s.string().key().identity(),
    userId: s.string().index("user_analytics"), // Indexed for fast user queries
    eventType: s.string("click", "view", "purchase").index("event_type_idx"), // Indexed for fast event queries
    timestamp: s.date().index("timestamp_idx"), // Indexed for time-based queries
    metadata: s.object({
        page: s.string(),
        referrer: s.string().optional(),
    }).serialize(obj => JSON.stringify(obj)) // Efficient serialization
        .deserialize(str => JSON.parse(str)),
}).modify(w => ({
    // Computed properties for common calculations
    isRecent: w.computed((entity) =>
        Date.now() - entity.timestamp.getTime() < 24 * 60 * 60 * 1000
    ).tracked(), // Tracked for fast reads
})).compile();

// Schemas enable automatic performance optimizations:
// - Database indexes for fast queries
// - Efficient serialization/deserialization
// - Computed properties cached for performance
// - Optimized data structures
// - Memory-efficient operations
// - Query optimization hints

7. Change Tracking and History

Schemas enable powerful change tracking features:

import { s } from "@routier/core/schema";

// Change tracking and history example
const documentSchema = s.define("documents", {
    id: s.string().key().identity(),
    title: s.string(),
    content: s.string(),
    author: s.string(),
    version: s.number().default(1),
    createdAt: s.date().default(() => new Date()).readonly(),
    updatedAt: s.date().default(() => new Date()),
}).modify(w => ({
    // Computed properties for change tracking
    hasChanges: w.computed((entity) =>
        entity.version > 1
    ).tracked(),

    // Function for history management
    getVersionInfo: w.function((entity) => ({
        version: entity.version,
        lastModified: entity.updatedAt,
        isModified: entity.version > 1,
    })),
})).compile();

// Schemas enable powerful change tracking features:
// - Automatic version management
// - Change detection and history
// - Audit trails and logging
// - Optimistic updates
// - Conflict resolution
// - Rollback capabilities
// - Change notifications

8. Serialization and Persistence

Schemas handle data transformation automatically:

import { s } from "@routier/core/schema";

// Serialization and persistence example
const settingsSchema = s.define("settings", {
    id: s.string().key().identity(),
    userId: s.string().distinct(),
    preferences: s.object({
        theme: s.string("light", "dark", "auto").default("auto"),
        language: s.string("en", "es", "fr").default("en"),
        notifications: s.boolean().default(true),
    }).serialize(obj => JSON.stringify(obj)) // Custom serialization
        .deserialize(str => JSON.parse(str)),
    lastLogin: s.date().serialize(date => date.toISOString()) // Date serialization
        .deserialize(str => new Date(str)),
    sessionData: s.object({
        token: s.string(),
        expires: s.date(),
    }).optional().serialize(obj => btoa(JSON.stringify(obj))) // Base64 encoding
        .deserialize(str => JSON.parse(atob(str))),
}).compile();

// Schemas handle data transformation automatically:
// - JSON serialization/deserialization
// - Date format conversion
// - Custom encoding (Base64, etc.)
// - Type conversion and validation
// - Default value application
// - Constraint enforcement
// - Cross-platform compatibility

Real-World Examples

E-commerce Application

import { s } from "@routier/core/schema";

// E-commerce application schema example
const ecommerceSchema = s.define("ecommerce", {
    // Products
    products: s.object({
        id: s.string().key().identity(),
        name: s.string(),
        description: s.string().optional(),
        price: s.number(),
        category: s.string("electronics", "books", "clothing", "home"),
        sku: s.string().distinct(),
        inStock: s.boolean().default(true),
        tags: s.array(s.string()).default([]),
    }),

    // Orders
    orders: s.object({
        id: s.string().key().identity(),
        customerId: s.string().index("customer_orders"),
        items: s.array(s.object({
            productId: s.string(),
            quantity: s.number(),
            price: s.number(),
        })),
        status: s.string("pending", "processing", "shipped", "delivered", "cancelled"),
        total: s.number(),
        createdAt: s.date().default(() => new Date()),
    }),

    // Customers
    customers: s.object({
        id: s.string().key().identity(),
        email: s.string().distinct(),
        name: s.string(),
        address: s.object({
            street: s.string(),
            city: s.string(),
            zipCode: s.string(),
            country: s.string(),
        }).optional(),
        preferences: s.object({
            newsletter: s.boolean().default(false),
            language: s.string("en", "es", "fr").default("en"),
        }).default({}),
    }),
}).compile();

// This schema provides:
// - Type safety for all e-commerce data
// - Automatic indexing for fast queries
// - Constraint enforcement (unique SKUs, emails)
// - Default values and computed properties
// - Consistent data handling across the application

User Management System

import { s } from "@routier/core/schema";

// User management system schema example
const userManagementSchema = s.define("userManagement", {
    users: s.object({
        id: s.string().key().identity(),
        email: s.string().distinct(),
        username: s.string().distinct(),
        passwordHash: s.string(),
        profile: s.object({
            firstName: s.string(),
            lastName: s.string(),
            avatar: s.string().optional(),
            bio: s.string().optional(),
        }),
        role: s.string("admin", "moderator", "user").default("user"),
        isActive: s.boolean().default(true),
        lastLogin: s.date().optional(),
        createdAt: s.date().default(() => new Date()).readonly(),
    }),

    sessions: s.object({
        id: s.string().key().identity(),
        userId: s.string().index("user_sessions"),
        token: s.string().distinct(),
        expiresAt: s.date(),
        createdAt: s.date().default(() => new Date()),
    }),

    permissions: s.object({
        id: s.string().key().identity(),
        userId: s.string().index("user_permissions"),
        resource: s.string(),
        action: s.string("read", "write", "delete"),
        granted: s.boolean().default(false),
    }),
}).compile();

// This schema provides:
// - Secure user data management
// - Role-based access control
// - Session management
// - Permission tracking
// - Audit trails (createdAt, lastLogin)
// - Type safety for all user operations

When Not to Use Schemas

While schemas are powerful, they’re not always necessary:

Simple Data Structures

// Simple data structures - schemas may be overkill
const simpleConfig = {
    apiUrl: "https://api.example.com",
    timeout: 5000,
    retries: 3,
};

// For simple, static configuration objects,
// a plain JavaScript object is often sufficient
// and doesn't require the overhead of schema definition

Temporary Data

// Temporary data - schemas not needed
const tempData = {
    searchResults: [],
    isLoading: false,
    error: null,
    filters: {},
};

// For temporary UI state, form data, or cache,
// schemas add unnecessary complexity
// Plain objects or state management libraries
// are more appropriate

External API Responses

// External API responses - schemas may not be needed
interface ExternalApiResponse {
    data: any; // Unknown structure from external API
    status: string;
    timestamp: string;
}

// For data from external APIs with unknown or changing structure,
// schemas may be too rigid and require constant updates
// Consider using TypeScript interfaces or validation libraries instead

Best Practices

1. Start Simple

import { s } from "@routier/core/schema";

// Start simple - begin with basic schema
const simpleUserSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
}).compile();

// Add complexity gradually as needed:
// - Start with essential properties
// - Add modifiers only when required
// - Introduce computed properties later
// - Add constraints as business rules emerge

// Don't over-engineer from the start!

2. Check Structure Early

import { s } from "@routier/core/schema";

// Check structure early - validate your schema design
const productSchema = s.define("products", {
    // Essential properties first
    id: s.string().key().identity(),
    name: s.string(),
    price: s.number(),

    // Add constraints early to catch issues
    category: s.string("electronics", "books", "clothing"), // Constrain early
    sku: s.string().distinct(), // Ensure uniqueness

    // Optional properties
    description: s.string().optional(),
    tags: s.array(s.string()).default([]),
}).compile();

// Test your schema with real data early:
// - Create sample entities
// - Verify constraints work
// - Check computed properties
// - Validate serialization
// - Test edge cases

3. Use Computed Properties

import { s } from "@routier/core/schema";

// Use computed properties for derived data
const orderSchema = s.define("orders", {
    id: s.string().key().identity(),
    items: s.array(s.object({
        productId: s.string(),
        quantity: s.number(),
        price: s.number(),
    })),
    taxRate: s.number().default(0.08),
}).modify(w => ({
    // Computed properties for derived values
    subtotal: w.computed((entity) =>
        entity.items.reduce((sum, item) => sum + (item.quantity * item.price), 0)
    ).tracked(),

    tax: w.computed((entity) =>
        entity.subtotal * entity.taxRate
    ).tracked(),

    total: w.computed((entity) =>
        entity.subtotal + entity.tax
    ).tracked(),
})).compile();

// Computed properties:
// - Automatically calculate derived values
// - Stay in sync with source data
// - Can be cached for performance
// - Reduce data duplication
// - Simplify business logic

4. Leverage Type Inference

import { s, InferType, InferCreateType } from "@routier/core/schema";

// Leverage type inference for type safety
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
}).compile();

// TypeScript automatically infers types
type User = InferType<typeof userSchema>;
type CreateUser = InferCreateType<typeof userSchema>;

// Use inferred types for type safety
function createUser(userData: CreateUser): User {
    // TypeScript ensures correct data structure
    return userData as User;
}

function updateUser(user: User, updates: Partial<User>): User {
    // TypeScript ensures type-safe updates
    // In Routier, you modify properties directly on the proxy object
    if (updates.name !== undefined) user.name = updates.name;
    if (updates.email !== undefined) user.email = updates.email;
    if (updates.age !== undefined) user.age = updates.age;
    if (updates.isActive !== undefined) user.isActive = updates.isActive;
    return user;
}

// Type inference benefits:
// - Automatic type generation
// - Compile-time type checking
// - IntelliSense support
// - Refactoring safety
// - API consistency

Conclusion

Schemas in Routier provide a powerful foundation for building robust, type-safe, and performant applications. They offer:

  • Type Safety - Compile-time type checking and structure definition
  • Automatic Features - Indexing, change tracking, computed properties
  • Consistency - Uniform data handling across your application
  • Performance - Automatic optimizations and efficient queries
  • Maintainability - Self-documenting, living data definitions

By embracing schemas, you’ll build applications that are more reliable, performant, and easier to maintain. The initial investment in defining schemas pays dividends throughout your application’s lifecycle.

Next Steps