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 five standard operations (GET, GET /:id, POST, PUT /:id, DELETE /:id) against your Zod schema and MongoDB model automatically.
Basic Usage
Use the crud snippet or write it manually inside a Router:
import { Router } from "@/core/http/router.ts";
import { createCRUD } from "@/core/utils/crud.ts";
import { postSchema, postInputSchema, postModel } from "@/schemas/post.ts";
export default new Router("/", function posts(router) {
createCRUD({
router,
schema: postSchema,
insertSchema: postInputSchema,
model: postModel,
});
});Important: Because Thunder uses file-based routing, the file name itself (e.g., posts.ts as '/posts') automatically acts as the root path. You should use "/" as the base path in your router to avoid creating duplicated path (like /posts/posts).
Read more about this behavior in the File-Based Routing documentation.
This single call produces the following endpoints automatically:
| Method | Path | Description |
|---|---|---|
GET | /posts | List all records |
GET | /posts/:id | Retrieve a single record |
POST | /posts | Create a new record |
PUT | /posts/:id | Replace an existing record |
DELETE | /posts/:id | Delete a record |
Configuration Options
createCRUD accepts two arguments: a required config object and an optional options object.
Required Config
| Field | Type | Description |
|---|---|---|
router | Router | The router instance to attach routes to |
schema | ZodObject | The full document schema (used for response validation) |
insertSchema | ZodObject | The input schema (used for create validation) |
model | Collection | The MongoDB collection model |
updateSchema | ZodObject (optional) | A separate schema for update operations. Defaults to a partial of insertSchema. |
Options
| Field | Type | Description |
|---|---|---|
disable | object | Disable specific CRUD methods by setting them to true |
isolationFields | (req: Request) => object | A function that returns additional fields to merge into every write operation |
Disabling Specific Methods
If you need custom logic for a particular endpoint, disable it from createCRUD and define it manually afterward:
export default new Router("/posts", function posts(router) {
createCRUD(
{
router,
schema: postSchema,
insertSchema: postInputSchema,
model: postModel,
},
{
disable: {
get: true, // Disable the GET /posts list endpoint
},
}
);
// Define your custom GET handler here
router.get("/", function listPosts() {
// ...your custom logic
});
});Refer to the Routes documentation for full details on writing individual route handlers.
Isolation Fields
isolationFields allows you to automatically inject fields into every write operation that are not part of the user-facing input schema. This is particularly useful for multi-tenancy, user scoping, or ownership tracking.
For example, to automatically associate every created record with the authenticated user:
import { getSession } from "@/utils/session.ts";
export default new Router("/posts", function posts(router) {
createCRUD(
{
router,
schema: postSchema,
insertSchema: postInputSchema,
model: postModel,
},
{
isolationFields: (req) => {
const session = getSession(req);
return {
userId: session.userId,
};
},
}
);
});With this configuration, userId is silently injected into every insertOne and updateOne operation - even though it is not present in postInputSchema. All read queries will also be automatically scoped to the authenticated user's userId.
Remove the isolationFields function entirely (or omit it from the options object) if your use case does not require per-user data isolation.
Complete Example
import { Router } from "@/core/http/router.ts";
import { createCRUD } from "@/core/utils/crud.ts";
import { postSchema, postInputSchema, postModel } from "@/schemas/post.ts";
const getSession = (req: Request) => {
// Extract session from JWT or cookie
return { userId: "user_abc123" };
};
export default new Router("/", function posts(router) {
createCRUD(
{
router,
schema: postSchema,
insertSchema: postInputSchema,
model: postModel,
},
{
disable: {
get: true,
},
isolationFields: (req) => {
const session = getSession(req);
return {
userId: session.userId,
};
},
}
);
// Custom GET /posts - disabled above, now redefined with custom logic
router.get("/", function listPosts() {
const $return = z.array(postSchema);
return {
shape: () => ({ return: $return }),
handler: async (req: Request) => {
const { userId } = getSession(req);
const docs = await postModel.find({ userId }).toArray();
return Response.json(docs);
},
};
});
});