Documentation Index
Fetch the complete documentation index at: https://mintlify.com/meteor/meteor/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Meteor Methods are the framework’s remote procedure call (RPC) system. They provide a secure way to send data from clients to the server, perform operations, and return results. Methods integrate tightly with Meteor’s data system to enable Optimistic UI.
Think of Methods as POST requests to your server, but with automatic client-side simulation, latency compensation, and built-in security features.
What is a Method?
A Method is:
- An API endpoint that can be called from client or server
- Defined once in code shared between client and server
- Automatically simulated on the client for instant UI updates
- Re-run on the server for validation and persistence
Defining Methods
Basic Method Definition
import { Meteor } from 'meteor/meteor';
import SimpleSchema from 'simpl-schema';
import { Todos } from '/imports/api/todos';
Meteor.methods({
'todos.insert'(text, listId) {
// Validate arguments
new SimpleSchema({
text: { type: String },
listId: { type: String }
}).validate({ text, listId });
// Check authentication
if (!this.userId) {
throw new Meteor.Error('not-authorized');
}
// Perform the operation
return Todos.insert({
text,
listId,
checked: false,
createdAt: new Date(),
userId: this.userId
});
}
});
Using jam:method Package
For a more structured approach, use the jam:method package:
import { createMethod } from 'meteor/jam:method';
export const insertTodo = createMethod({
name: 'todos.insert',
schema: new SimpleSchema({
text: { type: String },
listId: { type: String }
}),
async run({ text, listId }) {
if (!this.userId) {
throw new Meteor.Error('not-authorized');
}
return await Todos.insertAsync({
text,
listId,
checked: false,
createdAt: new Date(),
userId: this.userId
});
}
});
Calling Methods
From the Client
// Using Meteor.call
Meteor.call('todos.insert', 'Buy groceries', listId, (error, result) => {
if (error) {
alert('Error: ' + error.reason);
} else {
console.log('Todo created with ID:', result);
}
});
// With jam:method
import { insertTodo } from '/imports/api/todos/methods';
insertTodo.call({ text: 'Buy groceries', listId }, (error, result) => {
if (error) {
alert('Error: ' + error.reason);
} else {
console.log('Todo created with ID:', result);
}
});
Using Promises
try {
const todoId = await Meteor.callAsync('todos.insert', 'Buy groceries', listId);
console.log('Created todo:', todoId);
} catch (error) {
console.error('Error creating todo:', error.reason);
}
From the Server
// Server-to-server calls are synchronous by default
const todoId = Meteor.call('todos.insert', 'Buy groceries', listId);
// Or use async version
const todoId = await Meteor.callAsync('todos.insert', 'Buy groceries', listId);
Method Context (this)
Inside a Method, this provides:
Meteor.methods({
'example.method'() {
// Current user ID (null if not logged in)
console.log(this.userId);
// Unique ID for this method invocation
console.log(this.invocation.id);
// True if running on client (simulation)
console.log(this.isSimulation);
// Connection information
console.log(this.connection.id);
console.log(this.connection.clientAddress);
// Unblock other methods from running
this.unblock();
// Set a different user ID (server only)
this.setUserId(someUserId);
}
});
Optimistic UI
Methods run twice:
- Client simulation - Immediate UI update
- Server execution - Validation and persistence
Meteor.methods({
'todos.setChecked'(todoId, checked) {
check(todoId, String);
check(checked, Boolean);
const todo = Todos.findOne(todoId);
if (this.isSimulation) {
// Client simulation - fast, optimistic
console.log('Running on client');
} else {
// Server execution - authoritative
console.log('Running on server');
// Security checks only on server
if (todo.userId !== this.userId) {
throw new Meteor.Error('not-authorized');
}
}
Todos.update(todoId, { $set: { checked } });
}
});
Error Handling
Throwing Errors
Meteor.methods({
'todos.remove'(todoId) {
const todo = Todos.findOne(todoId);
if (!todo) {
throw new Meteor.Error('not-found', 'Todo not found');
}
if (todo.userId !== this.userId) {
throw new Meteor.Error(
'not-authorized',
'You are not authorized to delete this todo',
// Optional details object
{ todoId, userId: this.userId }
);
}
Todos.remove(todoId);
}
});
Handling Errors
Meteor.call('todos.remove', todoId, (error) => {
if (error) {
console.error('Error:', error.error); // 'not-authorized'
console.error('Reason:', error.reason); // Human-readable message
console.error('Details:', error.details); // Optional details object
}
});
Validation
Using check()
import { check, Match } from 'meteor/check';
Meteor.methods({
'todos.update'(todoId, updates) {
check(todoId, String);
check(updates, {
text: Match.Optional(String),
checked: Match.Optional(Boolean)
});
// Method implementation
}
});
Using SimpleSchema
import SimpleSchema from 'simpl-schema';
Meteor.methods({
'todos.insert'(data) {
new SimpleSchema({
text: { type: String, min: 1, max: 200 },
listId: { type: String },
priority: { type: String, allowedValues: ['low', 'medium', 'high'] },
dueDate: { type: Date, optional: true }
}).validate(data);
// Data is now validated
}
});
Security Best Practices
Never Trust this.userId from Client
// ❌ Bad: Client could pass any userId
'setUserName'({ userId, name }) {
Meteor.users.update(userId, { $set: { name } });
}
// ✅ Good: Use this.userId from DDP
'setUserName'({ name }) {
if (!this.userId) {
throw new Meteor.Error('not-authorized');
}
Meteor.users.update(this.userId, { $set: { name } });
}
One Method Per Action
// ✅ Good: Specific, testable methods
Meteor.methods({
'lists.makePrivate'(listId) { /* ... */ },
'lists.makePublic'(listId) { /* ... */ }
});
// ❌ Bad: Too generic, hard to secure
Meteor.methods({
'lists.setPrivacy'(listId, isPrivate) { /* ... */ }
});
Rate Limiting
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
const METHODS = [
'todos.insert',
'todos.update',
'todos.remove'
];
if (Meteor.isServer) {
DDPRateLimiter.addRule({
name(name) {
return METHODS.includes(name);
},
connectionId() { return true; }
}, 5, 1000); // 5 calls per second per connection
}
Advanced Patterns
Unblocking Methods
Meteor.methods({
async 'longRunningTask'() {
// Allow other methods to run
this.unblock();
// Perform long operation
const result = await someAsyncOperation();
return result;
}
});
Simulation Control
Meteor.methods({
'admin.deleteEverything'() {
// Don't simulate on client
if (this.isSimulation) {
return;
}
// Server-only operation
if (!Roles.userIsInRole(this.userId, 'admin')) {
throw new Meteor.Error('not-authorized');
}
// Dangerous operation
Todos.remove({});
}
});
Method Chaining
const createMethod = createMethod({
name: 'todos.createWithNotification',
schema: TodoSchema,
async run(todoData) {
// Call another method
const todoId = await this.call('todos.insert', todoData);
// Send notification
await this.call('notifications.send', {
userId: this.userId,
message: 'Todo created'
});
return todoId;
}
});
Testing Methods
import { assert } from 'chai';
import { Meteor } from 'meteor/meteor';
import { resetDatabase } from 'meteor/xolvio:cleaner';
import { insertTodo } from './methods';
if (Meteor.isServer) {
describe('todos.insert', function() {
beforeEach(function() {
resetDatabase();
});
it('creates a todo', function() {
const todoId = insertTodo.run.call(
{ userId: 'testUser' },
{ text: 'Test todo', listId: 'testList' }
);
assert.isString(todoId);
const todo = Todos.findOne(todoId);
assert.equal(todo.text, 'Test todo');
});
it('requires authentication', function() {
assert.throws(() => {
insertTodo.run.call(
{ userId: null },
{ text: 'Test todo', listId: 'testList' }
);
}, /not-authorized/);
});
});
}