Skip to main content
Modules are the fundamental building blocks of a Modelence application. They help you organize your backend functionality into cohesive, self-contained units that encapsulate queries, mutations, stores, and configuration.

What is a Module?

A Module in Modelence is similar to a feature module in other frameworks. It groups related functionality together, making your codebase more maintainable and easier to reason about.
import { Module } from 'modelence/server';

export default new Module('todo', {
  // Module configuration goes here
});

Module Structure

A typical module includes:
  • Stores - MongoDB collection definitions
  • Queries - Read operations that fetch data
  • Mutations - Write operations that modify data
  • Configuration - Module-specific settings
  • Cron Jobs - Scheduled tasks (optional)
Data migrations are configured at the startApp() level (not inside individual modules). See the Migrations documentation.

Stores

Stores define your MongoDB collections with schemas, indexes, and custom methods. Including stores in your module ensures they’re automatically provisioned when the server starts.
import { Store, schema } from 'modelence/server';

export const dbTodos = new Store('todos', {
  schema: {
    title: schema.string(),
    isCompleted: schema.boolean(),
    userId: schema.userId(),
    createdAt: schema.date()
  },
  indexes: [
    { key: { userId: 1 } }
  ]
});

export default new Module('todo', {
  // Register the store with the module
  stores: [dbTodos],

  queries: {
    async getAll({}, { user }) {
      return await dbTodos.fetch({ userId: user.id });
    }
  }
});
Learn more about working with Stores in the Stores documentation.

Authentication & Authorization

You can restrict access to queries and mutations using authentication requirements:
export default new Module('todo', {
  queries: {
    getAll: {
      // Require authentication for this query
      auth: true,
      async handler({}, { user }) {
        // user is guaranteed to exist here
        return await dbTodos.fetch({ userId: user.id });
      }
    },

    getPublic: {
      // No authentication required
      auth: false,
      async handler() {
        return await dbTodos.fetch({ isPublic: true });
      }
    }
  },

  mutations: {
    adminDelete: {
      // Custom authorization check
      auth: true,
      authorize: ({ user }) => {
        if (!user.roles?.includes('admin')) {
          throw new Error('Admin access required');
        }
      },
      async handler({ id }) {
        return await dbTodos.deleteOne({ id });
      }
    }
  }
});

Queries

Queries are read operations that fetch data without modifying state. For full query patterns, including client usage with callMethod, modelenceQuery, and typed client modules, see Queries.

Mutations

Mutations are write operations that create, update, or delete data. For full mutation patterns, including client usage with callMethod, modelenceMutation, and typed client modules, see Mutations.

Rate Limiting

Protect your queries and mutations from abuse by declaring rate limit rules on the module and consuming them inside handlers:
import { Module, consumeRateLimit } from 'modelence/server';
import { time } from 'modelence/server';

export default new Module('todo', {
  rateLimits: [
    { bucket: 'todoCreate', type: 'ip', window: time.minutes(1), limit: 10 },
  ],

  mutations: {
    async create({ title }, { user, connectionInfo }) {
      await consumeRateLimit({
        bucket: 'todoCreate',
        type: 'ip',
        value: connectionInfo.ip,
        message: 'Too many todos created. Please slow down.',
      });

      const { insertedId } = await dbTodos.insertOne({
        title,
        userId: user.id,
        isCompleted: false,
        createdAt: new Date()
      });
      return insertedId;
    }
  }
});
Learn more about rate limiting in the Rate Limiting documentation.

Best Practices

1. Keep Modules Focused

Each module should represent a single domain or feature:
// Good: Focused todo module
export default new Module('todo', {
  // Only todo-related functionality
});

// Good: Separate user module
export default new Module('users', {
  // Only user-related functionality
});

2. Use Clear Naming

Name your queries and mutations descriptively:
// Good
queries: {
  getAll() { },
  getByStatus({ status }) { },
  getOverdue() { }
}

// Avoid
queries: {
  get() { },           // Too generic
  fetchData() { },     // Unclear what data
  q1() { }            // Meaningless name
}

3. Handle Errors Gracefully

Always handle potential errors and provide meaningful messages:
mutations: {
  async delete({ id }, { user }) {
    const todo = await dbTodos.findById(id);

    if (!todo) {
      throw new Error('Todo not found');
    }

    if (todo.userId !== user.id) {
      throw new Error('Not authorized to delete this todo');
    }

    return await dbTodos.deleteOne({ id });
  }
}

4. Keep Business Logic in Modules

Don’t put business logic directly in your database stores. Keep it in your module methods:
// Good: Business logic in module
export default new Module('todo', {
  mutations: {
    async complete({ id }, { user }) {
      const todo = await dbTodos.findById(id);

      // Business logic
      if (todo.isCompleted) {
        throw new Error('Todo is already completed');
      }

      await dbTodos.updateOne({ id }, {
        $set: {
          isCompleted: true,
          completedAt: new Date()
        }
      });

      // Trigger side effects
      await callMethod('notifications.send', {
        userId: user.id,
        message: 'Todo completed!'
      });
    }
  }
});

5. Use TypeScript Types

Leverage TypeScript for type safety across your modules:
import { schema } from 'modelence/server';

type TodoPriority = 'low' | 'medium' | 'high';

export default new Module('todo', {
  mutations: {
    create: {
      input: {
        title: schema.string(),
        priority: schema.enum<TodoPriority>(['low', 'medium', 'high'])
      },
      async handler(input, { user }) {
        // input is fully typed
        const { insertedId } = await dbTodos.insertOne({
          ...input,
          userId: user.id,
          createdAt: new Date()
        });
        return insertedId;
      }
    }
  }
});

Next Steps

Queries

Learn how to define and call query methods

Mutations

Learn how to define and call mutation methods

Migrations

Learn how migration scripts run and how to handle cron race conditions

Configuration

Learn about configuration options

Stores

Deep dive into working with MongoDB stores