Building a Module
Create a reusable module — collections, globals, jobs, routes, services, and more.
A module is a reusable package that contributes entities to any QUESTPIE project. Modules are static objects -- they describe what exists, and the framework merges them at startup.
Module Structure
import { module, collection, global, job, route } from "questpie";
import { z } from "zod";
// Define collections
const notifications = collection("notifications")
.fields(({ f }) => ({
title: f.text({ required: true }),
body: f.textarea(),
read: f.boolean({ default: false }),
userId: f.relation(() => users),
}))
.hooks({
afterCreate: async ({ data, queue }) => {
await queue.sendPushNotification.publish({
userId: data.userId,
title: data.title,
body: data.body,
});
},
})
.admin(({ c }) => ({
label: { en: "Notifications" },
icon: c.icon("ph:bell"),
}));
// Define jobs
const sendPushNotification = job({
name: "sendPushNotification",
schema: z.object({
userId: z.string(),
title: z.string(),
body: z.string(),
}),
handler: async ({ payload }) => {
// Send push notification via external service
},
});
// Define routes
const markAllRead = route()
.post()
.schema(z.object({ userId: z.string() }))
.handler(async ({ input, collections }) => {
// Mark all notifications as read for a user
return { ok: true };
});
// Export the module
export const notificationsModule = module({
name: "notifications",
collections: {
notifications,
},
jobs: {
sendPushNotification,
},
routes: {
markAllRead,
},
sidebar: {
items: [
{
sectionId: "operations",
type: "collection",
collection: "notifications",
},
],
},
messages: {
en: {
"notifications.title": "Notifications",
"notifications.markRead": "Mark as read",
"notifications.empty": "No notifications",
},
sk: {
"notifications.title": "Notifikacie",
"notifications.markRead": "Oznacit ako precitane",
},
},
});Using the Module
Add the module to your modules.ts file:
import { adminModule } from "@questpie/admin/server";
import { notificationsModule } from "questpie-notifications";
export default [adminModule, notificationsModule] as const;After running questpie generate, all module contributions are merged into the generated types and factories.
Module Options
| Property | Type | Description |
|---|---|---|
name | string | Unique module identifier |
modules | ModuleDefinition[] | Dependency modules (resolved depth-first) |
collections | Record<string, Collection> | Collection contributions |
globals | Record<string, Global> | Global contributions |
jobs | Record<string, JobDefinition> | Background job definitions |
routes | Record<string, RouteDefinition> | API route definitions |
services | Record<string, ServiceBuilder> | Service contributions |
fields | Record<string, FieldFactory> | Custom field type factories |
auth | BetterAuthOptions | Auth configuration (deep-merged) |
migrations | Migration[] | Database migrations |
seeds | Seed[] | Seed data |
messages | Record<string, Record<string, string>> | i18n translations |
defaultAccess | CollectionAccess | Default access control rules |
hooks | GlobalHooksState | Global lifecycle hooks (concatenated) |
plugin | CodegenPlugin | CodegenPlugin[] | Codegen plugin contributions |
sidebar | object | Admin sidebar items |
dashboard | object | Admin dashboard widgets |
Module Dependencies
Modules can depend on other modules. Dependencies are resolved depth-first:
export const ecommerceModule = module({
name: "ecommerce",
modules: [notificationsModule, paymentsModule],
collections: {
products: productsCollection,
orders: ordersCollection,
},
});When a module lists dependencies, those modules are merged first, ensuring their collections, routes, and types are available.
Contributing a Codegen Plugin
Modules can contribute codegen plugins that define new file conventions:
export const myModule = module({
name: "my-module",
plugin: {
name: "my-module",
categories: {
templates: {
pattern: "templates/**/*.ts",
cardinality: "map",
mergeStrategy: "record",
registryKey: "templates",
},
},
},
// ... other contributions
});See Plugin API for the full plugin reference.
Merge Behavior
When multiple modules contribute to the same key, the merge behavior depends on the property type:
| Property | Merge Strategy |
|---|---|
collections | Later modules override by collection name |
globals | Later modules override by global name |
jobs | Later modules override by job name |
routes | Later modules override by route key |
services | Later modules override by service name |
fields | Later modules override by field type name |
auth | Deep-merged across all modules |
migrations | Concatenated (all migrations run) |
seeds | Concatenated (all seeds run) |
messages | Deep-merged per locale |
hooks | Concatenated (all hooks run in order) |
Publishing as a Package
Package your module as an npm package for reuse:
{
"name": "questpie-notifications",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"peerDependencies": {
"questpie": "^2.0.0"
}
}Export the module from your package entry point:
export { notificationsModule } from "./module";Users install it and add it to their modules.ts:
bun add questpie-notificationsRelated Pages
- Modules -- Module architecture
- Plugin API -- Codegen plugin reference
- Custom Fields -- Custom field types