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):
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:
| Method | Parameters | Returns |
|---|---|---|
findMany | args?: { query?, filters?, orderBy?, where? } | Promise<{ edges: ContentListRow<T>[], pageInfo: ContentPageInfo }> |
findById | id: number, options?: ContentServiceOptions | Promise<ContentSelect<T> | null> |
findRowById | id: number, options?: ContentServiceOptions | Promise<ContentListRow<T> | null> (row with labels) |
findDetail | id: number, options?: ContentServiceOptions | Promise<ContentDetail<T> | null> |
create | values: ContentCreateInput<T>, options?: ContentServiceOptions | Promise<ContentSelect<T>> |
update | id: number, values: ContentUpdateInput<T>, options?: ContentServiceOptions | Promise<ContentUpdateResult<T> | null> ({ row, changedFields }) |
delete | id: number, options?: ContentServiceOptions | Promise<ContentSelect<T> | null> (returns deleted row, or null) |
publish | id: number, options?: ContentServiceOptions | Promise<ContentPublicationResult<T> | null> (when publication enabled) |
unpublish | id: number, options?: ContentServiceOptions | Promise<ContentPublicationResult<T> | null> (when publication enabled) |
advanced | id: number, options?: ContentServiceOptions | Promise<ContentAdvancedValues<T>> (all relation and repeatable fields) |
options | field, 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:
| Schema | Purpose |
|---|---|
schemas.create | Validates new record input, checking required fields and defaults. |
schemas.update | Validates record updates with partial fields. |
schemas.select | Validates and serializes the complete database row. |
schemas.filter | Validates equality filter parameters for the content type. |
schemas.findManyQuery | Validates 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:
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:
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 Name | Payload Shape | Emitted 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
Database & Migrations
How the Content Engine maps content models to PostgreSQL tables using createContentModel, handles system columns, and executes Drizzle Kit migrations.
Publication & Editorial
Add draft/published lifecycles, revision histories, signed preview links, and scheduled publishing to content types.