Dyrected
Examples & RecipesApplication Patterns

Data Lifecycle

Application patterns for slugs, validation, and schema changes that need to stay safe as content changes over time.

Use these patterns when the hard part is not the initial schema, but keeping data safe and predictable as editors create, update, and evolve content. They help protect URL structure, catch invalid changes, and handle schema evolution more carefully.

For self-hosted Dyrected, lifecycle patterns can use your application runtime directly. That is the right fit for function hooks, database migrations, transaction-aware changes, and side effects that must run inside your server.

Generate a slug from a title

Problem: you want readable URLs, but you do not want editors hand-authoring slugs for every document.

This pattern generates the slug on the server and can also mirror it live in the Admin UI. It is a strong default for article pages, documentation content, and any collection that needs stable URL-friendly paths.

Example implementation

import { defineCollection, defineTextField } from "@dyrected/core";

export const toSlug = (value: unknown) =>
  String(value ?? "")
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/(^-|-$)/g, "");

export const Posts = defineCollection({
  slug: "posts",
  hooks: {
    beforeChange: [
      ({ data, operation }) => {
        if (operation === "create" || data.title !== undefined) {
          return { ...data, slug: toSlug(data.title) };
        }
        return data;
      },
    ],
  },
  fields: [
    defineTextField({ name: "title", label: "Title", required: true }),
    defineTextField({
      name: "slug",
      label: "Slug",
      required: true,
      unique: true,
      promoted: true,
      admin: {
        hooks: {
          onChange:
            "value == '' || value == null ? (siblingData.title != null ? slugify(siblingData.title) : value) : value",
        },
      },
    }),
  ],
});

[!TIP] You can use Dyrected's built-in Jexl helper functions—such as slugify(siblingData.title) or lower(siblingData.name)—directly in declarative string hooks for 100% Cloud-safe live field derivation.

Read the full docs:

Problem: one field value is only valid in relation to another field, such as a start date and an end date.

This pattern uses a collection hook to reject invalid combinations before they reach the database. It is a good fit for dates, time windows, numeric ranges, and any rule that depends on multiple values together.

Example implementation

import { defineCollection, defineDateTimeField, defineTextField } from "@dyrected/core";

export const Events = defineCollection({
  slug: "events",
  hooks: {
    beforeChange: [
      ({ data, doc }) => {
        const startsAt = data.startsAt ?? doc?.startsAt;
        const endsAt = data.endsAt ?? doc?.endsAt;

        const start = startsAt ? new Date(startsAt) : undefined;
        const end = endsAt ? new Date(endsAt) : undefined;

        if (start && Number.isNaN(start.getTime())) {
          throw new Error("The event start time must be a valid date.");
        }
        if (end && Number.isNaN(end.getTime())) {
          throw new Error("The event end time must be a valid date.");
        }
        if (start && end && end <= start) {
          throw new Error("The event end time must be after its start time.");
        }

        return data;
      },
    ],
  },
  fields: [
    defineTextField({ name: "title", label: "Title", required: true }),
    defineDateTimeField({
      name: "startsAt",
      label: "Starts at",
      required: true,
    }),
    defineDateTimeField({
      name: "endsAt",
      label: "Ends at",
      required: true,
    }),
  ],
});

Read the full docs:

Rename a field without orphaning existing data

Problem: you need to change a field name on a live schema without breaking the documents that already exist.

This pattern uses renameTo and a safe default so old content can continue working while the schema migrates. It is the safer path when a schema needs to evolve without forcing a risky big-bang rename.

Example implementation

import { defineCollection, defineTextField } from "@dyrected/core";

export const Customers = defineCollection({
  slug: "customers",
  fields: [
    defineTextField({
      name: "fullName",
      label: "Full name",
      renameTo: "name",
      defaultValue: "",
      required: true,
    }),
  ],
});

Read the full docs:

Archive records instead of deleting them

Problem: content should disappear from normal views without being permanently deleted from the database.

This pattern adds an archive flag, hides archived rows from normal readers, and disables destructive deletion. It is useful for announcements, listings, or internal records that may need to be restored or audited later.

Example implementation

import { defineBooleanField, defineCollection, defineTextField } from "@dyrected/core";

export const Announcements = defineCollection({
  slug: "announcements",
  access: {
    read: ({ user }) =>
      user?.roles?.includes("admin") ? true : { archived: { equals: false } },
    create: ({ user }) => Boolean(user),
    update: ({ user }) => Boolean(user),
    delete: () => false,
  },
  fields: [
    defineTextField({ name: "title", label: "Title", required: true }),
    defineBooleanField({
      name: "archived",
      label: "Archived",
      defaultValue: false,
    }),
  ],
});

Read the full docs:

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend