The Todo Model

Define the Todo schema and MongoDB model with Zod.

Every Thunder model is a Zod schema bound to a MongoDB collection. We'll separate the input schema ($todoInput - what the client sends) from the document schema ($todo - what's stored), following the Models & Schemas convention.

Define the Schema

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

// Fields accepted from the client
export const $todoInput = z.object({
  title: z.string().min(1).max(200),
  completed: z.boolean().default(false),
});

// Full document, including server-managed fields
export const $todo = $todoInput.extend({
  _id: $objectId.optional(),
  createdAt: z.date().default(() => new Date()),
});

// Bind the schema to the "todos" collection
export const todoModel = mongodb.db().collection<
  z.infer<typeof $todo>
>("todos");

Why Two Schemas?

SchemaUsed for
$todoInputValidating request bodies on create/update. Clients never set _id or createdAt.
$todoValidating full documents and response shapes; binds the collection type.

When inserting, validate with $todo.strictParse(...) so defaults like completed and createdAt are applied automatically. See strictParse.

(Optional) Add an Index

Todos are often listed newest-first, so add an index for createdAt. Per Thunder's serverless-first model, indexes are created via a script:

scripts/createIndexes.ts
import { todoModel } from "@/schemas/todo.ts";

await todoModel.createIndex({ createdAt: -1 });
console.log("✓ Indexes created");
deno run -A scripts/createIndexes.ts

With the model in place, let's build the API.


On this page