Skip to content

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.

typescript
// 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 include maxLength, minLength.
  • types.number(options): For numeric data. Options include min, max.
  • types.boolean(options): For strict true/false values.
  • 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 a UniqueConstraintError if 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().

typescript
// 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.

typescript
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.

  1. On Create: It automatically injects the ID of the currently authenticated user into the record.
  2. On Read/Update: It intercepts the request and ensures the client can only access records where the ownerField matches 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!

typescript
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.