DeesseJS Collections

Fields

Comprehensive guide to field configuration, validation, and customization

Fields

Fields are the building blocks of Collections. They define the schema of your data, control how it's validated, determine how it's stored in the database, and govern how it appears in your APIs.

Fields can be endlessly customized in their appearance and behavior without affecting their underlying data structure. They are designed to be composable, extensible, and type-safe.

Overview

Each field combines:

  1. A Field Type - Determines what kind of data it accepts (text, number, relation, etc.)
  2. Configuration - Labels, descriptions, defaults, validation rules
  3. Behavior - Hooks, computed values, conditional logic
  4. Presentation - Admin UI options, custom components

There are three main categories of fields in Collections:

  • Data Fields - Store data in the database
  • Computed Fields - Derive values from other fields
  • Relation Fields - Link to other collections

Basic Field Syntax

Fields are defined within a collection's fields property:

import { collection, field } from '@deessejs/collections'
import { text, email } from '@deessejs/collections/fields'

export const users = collection({
  slug: 'users',

  fields: {
    name: field({
      type: text({ min: 2, max: 100 }),
      label: 'Full Name',
      required: true
    }),

    email: field({
      type: email(),
      label: 'Email Address',
      unique: true,
      required: true
    })
  }
})

Field Configuration

Every field is configured with an object containing its type and options:

Prop

Type

Example:

field({
  type: text({ min: 2, max: 100 }),
  label: 'Name',
  description: 'Enter your full name',
  default: 'Anonymous',
  required: true,
  unique: true,
  indexed: true
})

Field Categories

Field Properties

name (implicit)

The field name is the key used in the fields object. This is the property name that will be used in TypeScript types and database columns:

fields: {
  // 'firstName' is the field name
  firstName: field({ type: text() })
}

// Usage:
user.firstName // Type-safe access

Field names must be unique within a collection.

type

Required. Defines the field type:

type: text({ min: 3, max: 255 })
type: email()
type: enumField(['draft', 'published'])
type: relation('users')

See Field Types for all available types.

label

Human-readable label for the field. Used in API responses, error messages, and Admin UI:

label: 'Email Address'

With i18n:

label: {
  en: 'Email Address',
  fr: 'Adresse e-mail',
  es: 'Dirección de correo'
}

If not provided, the field name will be formatted as the label (e.g., firstName → "First Name").

description

Additional context about the field. Shown in admin panels and API documentation:

description: 'We will never share your email with third parties'

With i18n:

description: {
  en: 'A short biography',
  fr: 'Une courte biographie'
}

default

Default value for the field. Applied on create operations if no value is provided:

// Static value
status: field({
  type: enumField(['draft', 'published']),
  default: 'draft'
})

// Function
createdAt: field({
  type: timestamp(),
  default: () => new Date()
})

required

Make a field required (cannot be null or undefined):

email: field({
  type: email(),
  required: true
})

Required fields are validated both at the API level and in the database schema.

unique

Add a unique constraint to the field in the database:

username: field({
  type: text(),
  unique: true
})

indexed

Add a database index for faster queries:

email: field({
  type: email(),
  indexed: true
})

Use indexed on fields that are frequently used in where clauses or for sorting.

nullable

Allow null values:

middleName: field({
  type: text(),
  nullable: true
})

Most fields are nullable by default. Use required: true to enforce non-null values.

hidden

Hide field from auto-generated forms and API responses:

apiKey: field({
  type: text(),
  hidden: true
})

Hidden fields are still stored in the database but not exposed publicly.

Validation

Built-in Validation

Fields are automatically validated based on their type:

// Text validation
name: field({
  type: text({ min: 2, max: 100 })
})

// Email validation
email: field({
  type: email()
})

// Number validation
age: field({
  type: number({ min: 0, max: 120 })
})

Custom Validation

Add custom validation logic with the validate function:

password: field({
  type: text({ min: 8 }),
  validate: (value) => {
    if (!/[A-Z]/.test(value)) {
      throw new Error('Password must contain at least one uppercase letter')
    }
    if (!/[0-9]/.test(value)) {
      throw new Error('Password must contain at least one number')
    }
    if (!/[!@#$%^&*]/.test(value)) {
      throw new Error('Password must contain at least one special character')
    }
    return true
  }
})

Validation Messages

Customize error messages with i18n support:

email: field({
  type: email(),
  messages: {
    required: {
      en: 'Email is required',
      fr: 'Email est requis',
      es: 'El correo electrónico es obligatorio'
    },
    invalid: {
      en: 'Please enter a valid email address',
      fr: 'Veuillez entrer une adresse e-mail valide',
      es: 'Por favor, introduzca una dirección de correo electrónico válida'
    }
  }
})

Available message keys:

Prop

Type

Computed Fields

Computed fields derive values from other fields without storing them in the database:

Basic Computed Field

fields: {
  price: field({ type: number() }),
  taxRate: field({ type: number(), default: 0.1 }),

  total: field({
    type: number(),
    computed: {
      from: ['price', 'taxRate'],
      compute: ({ price, taxRate }) => {
        return price + (price * taxRate)
      }
    }
  })
}

Computed Fields with Transformations

fields: {
  firstName: field({ type: text() }),
  lastName: field({ type: text() }),

  // Create URL-safe slug
  slug: field({
    type: text(),
    computed: {
      from: ['firstName', 'lastName'],
      compute: ({ firstName, lastName }) => {
        return `${firstName.toLowerCase()}-${lastName.toLowerCase()}`
      }
    }
  })
}

Computed fields are:

  • Read-only (cannot be set directly)
  • Calculated on every read operation
  • Not stored in the database
  • Available in API responses

Field Modifiers

Chain modifiers to fields for cleaner syntax:

email: field({
  type: email()
})
.required()
.unique()
.indexed()

Available modifiers:

  • .required() - Make field required
  • .unique() - Add unique constraint
  • .indexed() - Add database index
  • .default(value) - Set default value
  • .validate(fn) - Add custom validation

Admin Options

Customize how fields appear and behave in admin panels:

title: field({
  type: text(),
  admin: {
    placeholder: 'Enter your title...',
    helpText: 'Keep it under 60 characters for best display',
    width: '50%',
    style: {
      fontWeight: 'bold'
    },
    className: 'custom-title-field',
    readOnly: false,
    disabled: false
  }
})

Available admin options:

Prop

Type

Conditional Logic

Show or hide fields based on other field values:

fields: {
  hasDiscount: field({
    type: boolean(),
    default: false
  }),

  discountPercentage: field({
    type: number({ min: 0, max: 100 }),
    admin: {
      condition: (data, siblingData) => {
        return siblingData.hasDiscount === true
      }
    }
  })
}

Condition function receives:

  • data - The entire document's data
  • siblingData - Only fields within the same parent
  • user - Currently authenticated user (if available)

Field-Level Hooks

Execute logic at specific points in a field's lifecycle:

fields: {
  slug: field({
    type: text(),
    hooks: {
      beforeCreate: [({ data, value }) => {
        // Auto-generate slug from title
        return slugify(data.title)
      }],

      beforeUpdate: [({ data, value }) => {
        // Only regenerate if title changed
        if (data.titleChanged) {
          return slugify(data.title)
        }
        return value
      }]
    }
  })
}

Available field-level hooks:

  • beforeCreate - Before field is saved on create
  • afterCreate - After field is saved on create
  • beforeUpdate - Before field is saved on update
  • afterUpdate - After field is saved on update

Complete Example

Best Practices

  1. Use descriptive names - Field names should be clear and consistent
  2. Provide labels - Always include human-readable labels
  3. Add descriptions - Help users understand what each field does
  4. Validate input - Use built-in and custom validation
  5. Use i18n - Make your fields accessible to international users
  6. Index strategically - Add indexes to frequently queried fields
  7. Document computed fields - Explain how computed values are derived
  8. Use conditionals - Hide irrelevant fields to reduce complexity

Next Steps

  • Field Types - Explore all available field types
  • Collections - Learn about collections and advanced features
  • i18n - Internationalization support
  • Plugins - Extend functionality with plugins

On this page