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.