Create CRUD

Instantly scaffold a fully validated CRUD API for any model with a single function call.

createCRUD is one of Thunder's most powerful built-in utilities. It allows you to instantly generate a fully functional, validated CRUD (Create, Read, Update, Delete) API for any model - without writing a single individual route handler.

Under the hood, createCRUD wires up all standard operations against your Zod schema and MongoDB model automatically, including a count endpoint.


Basic Usage

Use the crud snippet or write it manually inside a Router:

routes/users.ts
import { Router } from "@/core/http/router.ts";
import { createCRUD, $objectId } from "@/core/utils/createCRUD.ts";
import { userModel, $user, $userInput } from "@/schemas/user.ts";
import z from "zod";

export default new Router("/api", function users(router) {
  createCRUD({
    router,
    schema: $user,
    model: userModel,
    insertSchema: $userInput,
  });
}).group("Users");

This single call produces the following endpoints automatically:

MethodPathDescription
POST/users/apiCreate a new record
GET/users/apiList all records
GET/users/api/:idRetrieve a single record
GET/users/api/countCount records
PATCH/users/api/:idUpdate an existing record
DELETE/users/api/:idDelete a record

Because Thunder uses file-based routing, the file name itself (e.g. users.ts) acts as the route prefix. The router base path ("/api" above) is appended after it, producing endpoints like /users/api. Read more in the File-Based Routing documentation.


Configuration Options

createCRUD accepts two arguments: a required config object and an optional options object.

Required Config

FieldTypeDescription
routerRouterThe router instance to attach routes to
schemaZodObjectThe full document schema (used for response validation)
modelCollectionThe MongoDB collection model
insertSchemaZodObjectThe input schema (used for create validation)
updateSchemaZodObject (optional)A narrowed schema for update operations. Defaults to a partial of insertSchema.

Options

FieldTypeDescription
disableobjectDisable specific endpoints by setting them to true (e.g. { del: true })
isolationFields(req, action) => objectAn async function that returns extra fields merged into writes and used to scope reads
hooksobjectLifecycle hooks: beforeCreate, afterCreate, etc.

Disabling Specific Methods

If you need custom logic for a particular endpoint, disable it from createCRUD and define it manually afterward:

routes/users.ts
export default new Router("/api", function users(router) {
  createCRUD(
    {
      router,
      schema: $user,
      model: userModel,
      insertSchema: $userInput,
    },
    {
      disable: { del: true }, // Disable the DELETE endpoint
    }
  );
}).group("Users");

Refer to the Routes documentation for full details on writing individual route handlers.


Isolation Fields

isolationFields automatically injects fields into every write operation that are not part of the user-facing input schema, and scopes read/update/delete queries to those same fields. This is particularly useful for multi-tenancy, user scoping, or ownership tracking.

The function is async and receives the request plus the current action ("create", "update", "del", "get", "count"), so you can return different fields per action:

routes/users.ts
export default new Router("/api", function users(router) {
  createCRUD(
    {
      router,
      schema: $user,
      model: userModel,
      insertSchema: $userInput,
      updateSchema: $userInput.pick({ name: true }),
    },
    {
      isolationFields: async (req, action) => {
        // On create, persist both org and user so they can be matched later.
        if (action === "create") {
          return {
            orgId: req.headers.get("X-Org-Id"),
            userId: req.headers.get("X-User-Id"),
          };
        }

        // Allow only the creator to edit or delete their own records.
        if (["update", "del"].includes(action)) {
          return {
            orgId: req.headers.get("X-Org-Id"),
            userId: req.headers.get("X-User-Id"),
          };
        }

        // For reads, scope by organization only.
        return {
          orgId: req.headers.get("X-Org-Id"),
        };
      },
    }
  );
}).group("Users");

In a real application the userId would usually come from the authenticated session (see thunder-core); the headers above are used for simplicity. Omit isolationFields entirely if you do not need per-user data isolation.


Lifecycle Hooks

Use hooks to run logic at key points in the CRUD lifecycle - for example, stamping records or triggering side effects:

routes/users.ts
createCRUD(
  {
    router,
    schema: $user,
    model: userModel,
    insertSchema: $userInput,
  },
  {
    hooks: {
      beforeCreate: ({ data }) => ({ ...data, createdBy: "system" }),
      afterCreate: ({ data }) => console.log("Created:", data._id),
    },
  }
);

Complete Example

routes/users.ts
import { Router } from "@/core/http/router.ts";
import { createCRUD, $objectId } from "@/core/utils/createCRUD.ts";
import { userModel, $user, $userInput } from "@/schemas/user.ts";
import z from "zod";

export default new Router("/api", function users(router) {
  createCRUD(
    {
      router,
      schema: $user,
      model: userModel,
      insertSchema: $userInput,
      updateSchema: $userInput.pick({ name: true }),
    },
    {
      disable: { del: true },
      isolationFields: async (req, action) => {
        if (action === "create") {
          return {
            orgId: req.headers.get("X-Org-Id"),
            userId: req.headers.get("X-User-Id"),
          };
        }
        return { orgId: req.headers.get("X-Org-Id") };
      },
      hooks: {
        beforeCreate: ({ data }) => ({ ...data, createdBy: "system" }),
        afterCreate: ({ data }) => console.log("Created:", data._id),
      },
    }
  );
}).group("Users"); // Groups endpoints for cleaner OpenAPI spec generation

Want to see all of this combined in a real, multi-tenant project - including thunder-core auth, an afterCreate notification, and a disabled route replaced with custom logic? Follow the Posts CMS example.


On this page