Creating A Schema
Schemas in Routier define the structure and behavior of your data entities. The schema builder provides a fluent, type-safe API for creating robust data schemas.
Quick Navigation
- Basic Schema Definition
- Schema Builder API
- Property Modifiers
- Complete Example
- Modifier Chaining
- Compiling Schemas
- Getting The Type Out
- Extending A Schema With
.modify() - Next Steps
Basic Schema Definition
import { s } from "@routier/core/schema";
// Basic schema definition
const userSchema = s
.define("users", {
id: s.string().key().identity(),
email: s.string().distinct(),
name: s.string(),
createdAt: s.date().default(() => new Date()),
})
.compile();Schema Builder API
The s object provides the main entry point for schema creation:
Core Functions
s.define(collectionName, schema)- Creates a schema definitions.number<T>()- Number property with optional literal constraintss.string(...literals)- String property with optional literal constraintss.string({ maxLength }, ...literals)- String plus a storage length declarations.boolean<T>()- Boolean propertys.date<T>()- Date propertys.array(schema)- Array property containing another schema propertys.object(schema)- Object property with nested schemas.file()- File input/reference output used with@routier/blob-plugins.vector(dimensions)- Fixed-width numeric embedding used with.nearest()
Literal Type Constraints
You can constrain properties to specific literal values:
import { s } from "@routier/core/schema";
// Literal type constraints
const statusSchema = s.define("orders", {
id: s.string().key().identity(),
status: s.string("pending", "processing", "completed", "cancelled"),
priority: s.number(1, 2, 3, 4, 5),
isActive: s.boolean(),
}).compile();Property Modifiers
Each schema type supports a set of modifiers that can be chained together:
Core Modifiers
import { s } from "@routier/core/schema";
// Core modifiers
const productSchema = s.define("products", {
id: s.string().key().identity(),
name: s.string().optional().nullable(),
price: s.number().default(0),
description: s.string().readonly(),
category: s.string().distinct(),
createdAt: s.date().default(() => new Date()),
}).compile();Serialization Modifiers
import { s } from "@routier/core/schema";
// Serialization modifiers
const configSchema = s.define("config", {
id: s.string().key().identity(),
settings: s.object({
theme: s.string(),
notifications: s.boolean(),
}).serialize((obj) => JSON.stringify(obj))
.deserialize((str) => JSON.parse(str)),
metadata: s.array(s.string()).serialize((arr) => arr.join(","))
.deserialize((str) => str.split(",")),
}).compile();Array and Object Modifiers
import { s } from "@routier/core/schema";
// Array and object modifiers
const companySchema = s.define("companies", {
id: s.string().key().identity(),
name: s.string(),
address: s.object({
street: s.string(),
city: s.string(),
zipCode: s.string(),
}).optional(),
departments: s.array(s.string()).default([]),
employees: s.array(s.object({
name: s.string(),
role: s.string(),
startDate: s.date(),
})).optional(),
}).compile();Complete Example
import { s } from "@routier/core/schema";
// Complete example with all features
const blogPostSchema = s.define("blogPosts", {
id: s.string().key().identity(),
title: s.string().distinct(),
content: s.string(),
author: s.string(),
tags: s.array(s.string()).default([]),
status: s.string("draft", "published", "archived").default("draft"),
publishedAt: s.date().optional(),
metadata: s.object({
views: s.number().default(0),
likes: s.number().default(0),
comments: s.array(s.object({
author: s.string(),
content: s.string(),
createdAt: s.date().default(() => new Date()),
})).default([]),
}).default({}),
createdAt: s.date().default(() => new Date()),
updatedAt: s.date().default(() => new Date()),
}).compile();Modifier Chaining
Modifiers can be chained in any order, but it's recommended to follow a logical pattern:
import { s } from "@routier/core/schema";
// Modifier chaining examples
const chainedSchema = s.define("examples", {
// Recommended order: type -> constraints -> behavior -> defaults
id: s.string().key().identity(),
email: s.string().distinct().optional(),
age: s.number(18, 19, 20, 21).default(18),
status: s.string("active", "inactive").default("active").readonly(),
// Complex chaining
profile: s.object({
bio: s.string().optional().nullable(),
avatar: s.string().optional(),
}).optional().serialize((obj) => JSON.stringify(obj))
.deserialize((str) => JSON.parse(str)),
}).compile();Compiling Schemas
Always call .compile() at the end to create the final schema:
import { InferType, s } from "@routier/core/schema";
// Compiling schemas
const userSchema = s.define("users", {
id: s.string().key().identity(),
name: s.string(),
email: s.string().distinct(),
}).compile();
// The compiled schema is ready to use.
//
// To get the ENTITY type, wrap it in InferType. `typeof userSchema` is the type of the
// schema object itself — it has no `id`, `name` or `email` on it.
type User = InferType<typeof userSchema>;
// User is { id: string; name: string; email: string }
const user: User = { id: "1", name: "James", email: "[email protected]" };Getting The Type Out
A compiled schema already describes the shape of your data, so you never write that shape a second time. InferType reads the entity type back off the schema, the way z.infer does in Zod:
type Product = InferType<typeof productSchema>;Note that typeof productSchema on its own is the type of the schema object, not the entity — it has no name or price on it. InferType is what unwraps it.
There is a second one worth knowing. InferCreateType is the shape you pass when adding an entity, which omits identity properties and anything with a default, because the store fills those in:
import { InferCreateType, InferType, s } from "@routier/core/schema";
const productSchema = s.define("products", {
id: s.string().key().identity(),
name: s.string(),
category: s.string("tool", "material"),
price: s.number(),
createdAt: s.date().default(() => new Date()),
}).compile();
// Declare the shape once, in the schema. These read it back off.
export type Product = InferType<typeof productSchema>;
export type NewProduct = InferCreateType<typeof productSchema>;
// Product is the stored entity: every property present, unions preserved.
const stored: Product = {
id: "p1",
name: "Hammer",
category: "tool",
price: 12,
createdAt: new Date(),
};
// NewProduct is what you pass to addAsync: `id` is generated and `createdAt` has a
// default, so neither is required here.
const toAdd: NewProduct = {
name: "Oak plank",
category: "material",
price: 4,
};
// Use them anywhere a type is expected — parameters, returns, React props, API payloads.
const priceOf = (product: Product): number => product.price;
// A schema change is a compile error everywhere the type is used, instead of a silent
// mismatch against a hand-written interface.
export { productSchema, stored, toAdd, priceOf };Reach for these instead of hand-writing an interface. A hand-written one has to be updated every time the schema changes, and nothing tells you when you have missed one.
See InferType for the full reference.
Extending A Schema With .modify()
.modify() runs after define() and adds properties that are derived from the entity rather than supplied by the caller. It receives three builders:
| Builder | Stored? | You get |
|---|---|---|
x.computed(fn) | No | The value fn returns, recomputed on read |
x.computed(fn).tracked() | Yes | The same value, persisted so the backend can filter and index it |
x.function(fn) | No | Whatever fn returns — return a function to get a method on the entity |
x.transform({ to, from, stores }) | Stored, converted | An existing property whose stored form differs from its in-memory form |
fn receives (entity, collectionName, injected). The collectionName argument is why documentType: x.computed((_, collectionName) => collectionName).tracked() is a common idiom — it tags rows when several collections share one physical store.
import { InferType, s } from "@routier/core/schema";
const orderSchema = s.define("orders", {
id: s.string().key().identity(),
quantity: s.number(),
unitPrice: s.number(),
}).modify(x => ({
// Derived on read, never stored. The database has no `total` column.
total: x.computed(order => order.quantity * order.unitPrice),
// Derived AND stored, so the backend can filter and index on it.
storedTotal: x.computed(order => order.quantity * order.unitPrice).tracked(),
// The collection name, which `.modify()` passes as the second argument. A common way to
// tag rows when several collections share one physical store.
documentType: x.computed((_order, collectionName) => collectionName).tracked(),
// Behavior rather than data: the outer function receives the entity, and whatever it
// RETURNS becomes the property. Return a function and you get a method on the entity.
describe: x.function(order => () => `${order.quantity} x ${order.unitPrice}`),
})).compile();
// All of it is part of the inferred type, so nothing needs restating by hand.
type Order = InferType<typeof orderSchema>;
const report = (order: Order) => ({
total: order.total, // number, computed on read
stored: order.storedTotal, // number, read back from storage
kind: order.documentType, // "orders"
text: order.describe(), // "2 x 9.99"
});
export { orderSchema, report };
export type { Order };Two things worth being clear about:
computed versus computed().tracked() is about the database, not the type. Both give you the same property to read. Only the tracked one exists as a column, so only the tracked one can be filtered or sorted in the backend rather than in memory.
x.function holds whatever your function returns. Returning a value gives you a value; returning a function gives you a method, which is the describe() case above. Unlike computed, it can never be .tracked() — behavior is not storable.
x.transform replaces an existing property under the same name, declaring both directions of a storage conversion. See Schema API for its options, and Encryption for a packaged one.
Next Steps
- Schema API - Complete factories and modifier compatibility
- Property Types - Detailed property type reference
- Modifiers - All available property modifiers
- InferType - Type inference and type safety
- Why Schemas? - Understanding the benefits of schemas