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?
- Benefits of Using Schemas
- Real-World Examples
- When Not to Use Schemas
- Best Practices
- Conclusion
- Next Steps
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 type2. 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 behavior4. 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 safety5. 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 everywhere6. 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 hints7. 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 notifications8. 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 compatibilityReal-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 applicationUser 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 operationsWhen 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 definitionTemporary 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 appropriateExternal 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 insteadBest 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 cases3. 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 logic4. 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 consistencyConclusion
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
- Creating A Schema - Learn how to create schemas
- Property Types - Explore available property types
- Modifiers - Understand property modifiers