Content Engine

Content Services & API

Call generated Content Engine services from custom Hono routes, use typed Zod schemas, and listen to content events.

When you compile a content type with createContentModel, it provides a typed service, Zod validation schemas, and database events that can be used directly inside custom Hono endpoints.

Quick start

Access the typed repository in any Hono route via model.service(c):

plugins/blog/src/api/modules/articles/routes/featured.route.ts
import { buildRoute } from "@vitnode/core/api/lib/route"
import { articleContent } from "@/content/articles"

export const featuredRoute = buildRoute({
  pluginId: "blog",
  route: {
    method: "get",
    path: "/featured",
    responses: { 200: { description: "Featured articles" } },
  },
  handler: async (c) => {
    const { edges, pageInfo } = await articleContent.service(c).findMany({
      filters: { featured: true },
      orderBy: { column: "createdAt", order: "desc" },
      query: { first: "5" },
    })

    return c.json({ edges, pageInfo })
  },
})

findMany returns { edges, pageInfo } formatted for cursor-based pagination. Each edge in edges includes the row fields along with resolved labels for relations and user references.


Service Methods Reference

The service returned by model.service(c) is typed to the content type's schema and capabilities:

MethodParametersReturns
findManyargs?: { query?, filters?, orderBy?, where? }Promise<{ edges: ContentListRow<T>[], pageInfo: ContentPageInfo }>
findByIdid: number, options?: ContentServiceOptionsPromise<ContentSelect<T> | null>
findRowByIdid: number, options?: ContentServiceOptionsPromise<ContentListRow<T> | null> (row with labels)
findDetailid: number, options?: ContentServiceOptionsPromise<ContentDetail<T> | null>
createvalues: ContentCreateInput<T>, options?: ContentServiceOptionsPromise<ContentSelect<T>>
updateid: number, values: ContentUpdateInput<T>, options?: ContentServiceOptionsPromise<ContentUpdateResult<T> | null> ({ row, changedFields })
deleteid: number, options?: ContentServiceOptionsPromise<ContentSelect<T> | null> (returns deleted row, or null)
publishid: number, options?: ContentServiceOptionsPromise<ContentPublicationResult<T> | null> (when publication enabled)
unpublishid: number, options?: ContentServiceOptionsPromise<ContentPublicationResult<T> | null> (when publication enabled)
advancedid: number, options?: ContentServiceOptionsPromise<ContentAdvancedValues<T>> (all relation and repeatable fields)
optionsfield, search?, ids?Promise<{ color?: string, label: string, value: number }[]>
relations[name]Collection methods: add, get, remove, reorder, set
repeatable[name]Nested row methods: create, delete, list, reorder, set, update

Database Transactions

All service methods accept an optional options argument with tx to participate in an existing database transaction:

await c.var.db.transaction(async (tx) => {
  const service = articleContent.service(c, { tx })
  const article = await service.create({ title: "Hello World" })
  // other transactional operations...
})

Generated Zod Schemas

createContentModel automatically compiles Zod validation schemas accessible under model.schemas:

SchemaPurpose
schemas.createValidates new record input, checking required fields and defaults.
schemas.updateValidates record updates with partial fields.
schemas.selectValidates and serializes the complete database row.
schemas.filterValidates equality filter parameters for the content type.
schemas.findManyQueryValidates standard cursor pagination parameters (cursor, first, last, search).

Use them in custom routes to validate incoming requests:

import { buildRoute } from "@vitnode/core/api/lib/route"
import { articleContent } from "@/content/articles"

export const createArticleRoute = buildRoute({
  pluginId: "blog",
  route: {
    method: "post",
    path: "/",
    request: {
      body: {
        content: {
          "application/json": {
            schema: articleContent.schemas.create,
          },
        },
      },
    },
  },
  handler: async (c) => {
    const data = c.req.valid("json")
    const article = await articleContent.service(c).create(data)
    return c.json(article, 201)
  },
})

Content Engine Events

Content modifications automatically emit typed domain events on the event bus using the pattern content.${contentTypeId}.${action}.

Register the content events in your plugin's TypeScript type definitions using module augmentation:

plugins/blog/src/events.d.ts
import type { ContentEventsFor } from "@vitnode/core/content"
import type { articleContentType } from "./content/articles"

declare module "@vitnode/core/api/models/events" {
  interface VitNodeEvents
    extends ContentEventsFor<typeof articleContentType> {}
}

Now event listeners have fully typed names and payloads:

plugins/blog/src/api/lib/listeners.ts
import { buildEventListener } from "@vitnode/core/api/lib/events"

export const onArticleCreated = buildEventListener({
  event: "content.blog.article.created",
  name: "notify-subscribers",
  handler: async (c, payload) => {
    // payload is typed: { contentId: number }
    await c.get("queue").dispatch({
      name: "broadcast-new-article",
      payload: { articleId: payload.contentId },
    })
  },
})

Event Reference

Event NamePayload ShapeEmitted When
content.${id}.created{ contentId: number }A new record is inserted.
content.${id}.updated{ contentId: number, changedFields: string[] }A record is modified.
content.${id}.deleted{ contentId: number }A record is permanently removed.
content.${id}.published{ contentId: number, publishedAt: Date }A record transitions to published.
content.${id}.unpublished{ contentId: number }A record transitions to draft.
content.${id}.restored{ contentId: number, revisionId: number, version: number }An editorial revision is restored.
content.${id}.scheduled{ contentId: number, action, scheduledFor, scheduleId }Publication is scheduled.
content.${id}.schedule_cancelled{ contentId: number, action, scheduleId }A scheduled job is cancelled.

When localization is enabled, corresponding content.${id}.translation_* events are also emitted for translation actions.

Learn More