QUESTPIE
Extend the Platform

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:

src/questpie/server/modules.ts
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

PropertyTypeDescription
namestringUnique module identifier
modulesModuleDefinition[]Dependency modules (resolved depth-first)
collectionsRecord<string, Collection>Collection contributions
globalsRecord<string, Global>Global contributions
jobsRecord<string, JobDefinition>Background job definitions
routesRecord<string, RouteDefinition>API route definitions
servicesRecord<string, ServiceBuilder>Service contributions
fieldsRecord<string, FieldFactory>Custom field type factories
authBetterAuthOptionsAuth configuration (deep-merged)
migrationsMigration[]Database migrations
seedsSeed[]Seed data
messagesRecord<string, Record<string, string>>i18n translations
defaultAccessCollectionAccessDefault access control rules
hooksGlobalHooksStateGlobal lifecycle hooks (concatenated)
pluginCodegenPlugin | CodegenPlugin[]Codegen plugin contributions
sidebarobjectAdmin sidebar items
dashboardobjectAdmin 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:

PropertyMerge Strategy
collectionsLater modules override by collection name
globalsLater modules override by global name
jobsLater modules override by job name
routesLater modules override by route key
servicesLater modules override by service name
fieldsLater modules override by field type name
authDeep-merged across all modules
migrationsConcatenated (all migrations run)
seedsConcatenated (all seeds run)
messagesDeep-merged per locale
hooksConcatenated (all hooks run in order)

Publishing as a Package

Package your module as an npm package for reuse:

package.json
{
  "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:

src/index.ts
export { notificationsModule } from "./module";

Users install it and add it to their modules.ts:

bun add questpie-notifications

On this page