Schemas & Models
At the heart of JSONExpress is the Model Schema. Instead of manually writing database migrations, SQL queries, and REST controllers, you simply define a declarative schema. The framework handles the rest.
Defining a Model
To create a new model, create a TypeScript file inside your project's /models directory. Export the result of the defineModel function provided by the core package.
// models/users.ts
import { defineModel, types } from '@json-express/core';
export default defineModel({
name: 'users',
fields: {
id: types.id(),
email: types.string({ unique: true }),
age: types.number({ min: 18 }),
isActive: types.boolean({ default: true })
}
});Supported Field Types
The types utility provides several built-in builders to ensure strict type safety across your framework:
types.string(options): For text data. Options includemaxLength,minLength.types.number(options): For numeric data. Options includemin,max.types.boolean(options): For stricttrue/falsevalues.types.date(options): For ISO-8601 date strings.types.id(): For primary keys (auto-generated by adapters if omitted).
Global Constraints
All field types accept a BaseOptions object which includes powerful constraints:
unique: boolean: Instructs the database adapter to throw aUniqueConstraintErrorif a duplicate value is inserted.required: boolean: Enforces that the field must exist on creation.default: any: Assigns a default value if the client omits the field.
Relational Data
JSONExpress provides a robust relational engine. You can define relations natively within your schemas using types.relation().
// models/posts.ts
import { defineModel, types } from '@json-express/core';
export default defineModel({
name: 'posts',
fields: {
id: types.id(),
title: types.string(),
author: types.relation({
target: 'users',
type: 'many-to-one',
foreignKey: 'authorId' // Optional: defaults to target + 'Id'
})
}
});When a client queries this model via the REST API (e.g., GET /posts?_expand=author), the database adapter will automatically execute the join and return the populated relational data!
Field-Level Security & Access Control
Security is built directly into the schema. You can explicitly allow or deny access to entire models, or strip out specific fields on a per-request basis.
export default defineModel({
name: 'users',
access: {
read: 'public', // Anyone can read
create: 'admin', // Only admins can create
update: 'owner', // Only the owner of the record can edit it
delete: 'admin'
},
fields: {
id: types.id(),
email: types.string(),
passwordHash: types.string({
// This field will be completely scrubbed from all API responses!
access: { read: false }
})
}
});The owner Rule
If an access rule is set to 'owner', JSONExpress performs automatic ownership enforcement.
- On Create: It automatically injects the ID of the currently authenticated user into the record.
- On Read/Update: It intercepts the request and ensures the client can only access records where the
ownerFieldmatches their JWT payload ID.
Custom Endpoints
While the API Generator handles standard CRUD operations, you often need custom business logic. You can bind custom, Express-like endpoints directly to the schema!
export default defineModel({
name: 'stats',
exposeApi: false, // Disables the auto-generated CRUD routes
endpoints: {
'GET /metrics': async (req, res, ctx) => {
const userCount = (await ctx.db.getAll('users')).length;
return res.status(200).json({ users: userCount });
}
},
fields: {}
});Common Questions
How do I use JSON files instead of TypeScript?
If you are rapidly prototyping, you can drop a .json file into the /data directory (e.g., /data/products.json). The JSONExpress CLI will automatically infer a basic schema from the JSON structure at boot time!
Can a plugin override a schema?
Yes. Plugins (like @json-express/plugin-identity) can inject their own highly-secure schemas into the framework. Plugin schemas will safely overwrite any inferred JSON schemas of the same name.