Models & Schemas

Define data schemas using Zod and interact with MongoDB using the native driver in Thunder.

Thunder uses Zod as its schema definition and validation library. Zod provides a TypeScript-first approach to declaring the shape of your data - from request bodies to database documents - with full static type inference.

For database interaction, Thunder integrates the native MongoDB driver directly, giving you full control over your queries without the overhead of an abstraction layer.

If you prefer to use a different database or ORM/ODM with Thunder, refer to the alternative database guide for integration options.

File Structure

Models live inside the schemas/ directory of your Thunder project. The recommended structure is:

user.ts
post.ts
comment.ts
database.ts

Creating a Model

Define a Zod Schema

Use z.object() to define the shape of your data. It is recommended to separate your input schema (what the user provides) from the full document schema (what gets stored in the database):

schemas/user.ts
import z from "zod";
import { mongodb } from "@/database.ts";

// Input schema - fields accepted from the client
export const userInputSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  password: z.string().min(8),
});

// Full document schema - includes server-managed fields
export const userSchema = userInputSchema.extend({
  createdAt: z.date().default(() => new Date()),
  updatedAt: z.date().default(() => new Date()),
});

Create the Model

Use the native MongoDB driver to bind your schema to a collection:

schemas/user.ts
export const userModel = mongodb.db().collection<
  z.infer<typeof userSchema>
>("users");

Using the Model

Once a model is defined, import and use it anywhere in your business logic:

routes/users.ts
import { userModel, userInputSchema } from "@/schemas/user.ts";

// Insert a new user
const { insertedId } = await userModel.insertOne({
  name: "Alice",
  email: "alice@example.com",
  password: "hashed_password",
  createdAt: new Date(),
  updatedAt: new Date(),
});

// Find a user by email
const user = await userModel.findOne({ email: "alice@example.com" });

Import Conventions

Always use the @/ import alias instead of relative paths. Relative imports into core/ or other framework internals are forbidden and will trigger a lint error:

Relative import into "core/" is forbidden. Use "@/core/..." instead.
deno-lint(no-relative-core-imports)

Correct:

import { userModel } from "@/schemas/user.ts";

Incorrect:

import { userModel } from "../schemas/user.ts"; // ❌ Avoid this

Complete Model Example

schemas/env.ts
import z from "zod";
import { mongodb } from "@/database.ts";

export const envInputSchema = z.object({
  key: z.string(),
  value: z.string(),
});

export const envSchema = envInputSchema.extend({
  createdAt: z.date().default(() => new Date()),
});

export const envModel = mongodb.db().collection<
  z.infer<typeof envSchema>
>("envs");

On this page