Model

Define the multi-tenant post schema with the fields that power isolation and soft-delete.

Create the post schema under schemas/cms/. Two fields do the heavy lifting later: tenant is what isolationFields will populate on every write, and deletedAt powers our custom soft-delete.

schemas/cms/post.ts
import z from "zod";
import { mongodb } from "@/database.ts";
import { $objectId } from "@/core/utils/createCRUD.ts";

// What clients send when creating/updating a post
export const postInputSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string(),
  published: z.boolean().default(false),
});

// The full stored document
export const postSchema = postInputSchema.extend({
  _id: $objectId.optional(),
  tenant: $objectId,                 // injected by isolationFields on create
  deletedAt: z.date().optional(),    // set by the custom soft-delete
  createdAt: z.date().default(() => new Date()),
});

export const postModel = mongodb.db().collection<
  z.infer<typeof postSchema>
>("posts");

Why two schemas?

  • postInputSchema describes the shape clients are allowed to send. It deliberately omits tenant, _id, and timestamps - a client should never set those.
  • postSchema extends the input with server-owned fields. createCRUD uses it to validate stored documents and to generate accurate response types.

$objectId is exported from @/core/utils/createCRUD.ts and validates MongoDB ObjectId values in Zod. See Models for more on the $-prefixed schema convention and ObjectId handling.

With the model in place, we can generate the entire API in a single call. On to the API.


On this page