Getting started
Installation
# Using npx (recommended)
npx meteor create myapp
# Or install globally
npm install -g meteor
meteor create myapp
Quick Start
# Create new app
meteor create myapp
# Run development server
cd myapp
meteor
# Opens at http://localhost:3000
Basic Structure
myapp/
├── imports/ # ES6 modules (lazy-loaded)
│ ├── api/ # Collections, publications, methods
│ └── ui/ # Components, templates
├── client/ # Runs in browser
├── server/ # Runs on server
├── public/ # Static assets
└── tests/ # Test files
First Collection
// imports/api/tasks.js
import { Mongo } from "meteor/mongo";
export const Tasks = new Mongo.Collection("tasks");
// Insert data
await Tasks.insertAsync({
text: "My first task",
createdAt: new Date(),
});
Collections (MongoDB)
Creating Collections
import { Mongo } from "meteor/mongo";
// Create collection
export const Tasks = new Mongo.Collection("tasks");
export const Users = new Mongo.Collection("users");
// In-memory (no persistence)
const LocalCache = new Mongo.Collection(null);
Insert Operations
// Insert single document (async)
const id = await Tasks.insertAsync({
text: "New task",
done: false,
createdAt: new Date(),
});
// Insert multiple
await Tasks.rawCollection().insertMany([
{ text: "Task 1" },
{ text: "Task 2" },
]);
Query Operations
// Find returns cursor
const cursor = Tasks.find({ done: false });
// Get array of documents
const tasks = await cursor.fetchAsync();
// Find one document
const task = await Tasks.findOneAsync({ _id: id });
// Count documents
const count = await Tasks.find({ done: true }).countAsync();
Update Operations
// Update single document
await Tasks.updateAsync({ _id: id }, { $set: { done: true } });
// Update multiple
await Tasks.updateAsync(
{ done: false },
{ $set: { priority: "low" } },
{ multi: true },
);
// Upsert (insert if not exists)
await Tasks.updateAsync(
{ text: "Unique task" },
{ $set: { done: false } },
{ upsert: true },
);
Delete Operations
// Remove single document
await Tasks.removeAsync({ _id: id });
// Remove multiple
await Tasks.removeAsync({ done: true });
// Remove all
await Tasks.removeAsync({});
Query Selectors
// Comparison operators
Tasks.find({ count: { $gt: 10 } });
Tasks.find({ status: { $in: ["active", "pending"] } });
// Logical operators
Tasks.find({
$and: [{ done: false }, { priority: "high" }],
});
// Regular expressions
Tasks.find({ text: /urgent/i });
// Existence
Tasks.find({ deletedAt: { $exists: false } });
Query Options
// Sort
Tasks.find({}, { sort: { createdAt: -1 } });
// Limit
Tasks.find({}, { limit: 10 });
// Skip (pagination)
Tasks.find({}, { skip: 20, limit: 10 });
// Field projection
Tasks.find(
{},
{
fields: { text: 1, done: 1 },
},
);
Publications & Subscriptions
Server Publications
// imports/api/tasks.js (server)
import { Meteor } from "meteor/meteor";
import { Tasks } from "./collections";
// Publish all tasks
Meteor.publish("tasks", function () {
return Tasks.find();
});
// Publish with filter
Meteor.publish("myTasks", function () {
if (!this.userId) {
return this.ready();
}
return Tasks.find({ owner: this.userId });
});
// Publish with parameters
Meteor.publish("tasksByStatus", function (status) {
return Tasks.find({ status });
});
Client Subscriptions
// Subscribe to publication
const handle = Meteor.subscribe("tasks");
// With parameters
Meteor.subscribe("tasksByStatus", "active");
// Check if ready
if (handle.ready()) {
console.log("Data loaded");
}
// Stop subscription
handle.stop();
React Integration
import { useTracker } from "meteor/react-meteor-data";
import { Tasks } from "../api/tasks";
function TaskList() {
const { tasks, loading } = useTracker(() => {
const handle = Meteor.subscribe("tasks");
return {
tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
loading: !handle.ready(),
};
});
if (loading) return <div>Loading...</div>;
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.text}</li>
))}
</ul>
);
}
Computed Publications
// Publish data from multiple collections
Meteor.publish("taskWithUser", function (taskId) {
const task = Tasks.findOne(taskId);
if (!task) return this.ready();
return [
Tasks.find({ _id: taskId }),
Meteor.users.find(
{ _id: task.owner },
{ fields: { username: 1, profile: 1 } },
),
];
});
Publish with Added/Changed
Meteor.publish("customTasks", function () {
let initializing = true;
const handle = Tasks.find().observeChanges({
added: (id, fields) => {
this.added("tasks", id, fields);
},
changed: (id, fields) => {
this.changed("tasks", id, fields);
},
removed: (id) => {
this.removed("tasks", id);
},
});
initializing = false;
this.ready();
this.onStop(() => handle.stop());
});
Methods (RPC)
Define Methods
// imports/api/methods.js
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
import { Tasks } from "./tasks";
Meteor.methods({
async "tasks.insert"(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error("not-authorized");
}
return await Tasks.insertAsync({
text,
owner: this.userId,
createdAt: new Date(),
});
},
async "tasks.remove"(taskId) {
check(taskId, String);
const task = await Tasks.findOneAsync(taskId);
if (task.owner !== this.userId) {
throw new Meteor.Error("not-authorized");
}
return await Tasks.removeAsync(taskId);
},
});
Call Methods
// Client-side call
try {
const id = await Meteor.callAsync("tasks.insert", "New task");
console.log("Inserted:", id);
} catch (error) {
console.error(error.message);
}
// With callback (legacy)
Meteor.call("tasks.insert", "New task", (error, result) => {
if (error) {
console.error(error);
} else {
console.log(result);
}
});
Validation
import { check, Match } from "meteor/check";
Meteor.methods({
async "tasks.update"(taskId, updates) {
// Type checking
check(taskId, String);
check(updates, {
text: Match.Optional(String),
done: Match.Optional(Boolean),
priority: Match.Optional(Match.OneOf("low", "medium", "high")),
});
await Tasks.updateAsync(taskId, {
$set: updates,
});
},
});
Method Context
Meteor.methods({
async "tasks.count"() {
// this.userId - current user ID
console.log("User:", this.userId);
// this.connection - connection info
console.log("IP:", this.connection.clientAddress);
// this.isSimulation - true on client
if (this.isSimulation) {
console.log("Running in simulation");
}
// this.unblock() - allow other methods
this.unblock();
return await Tasks.find().countAsync();
},
});
Error Handling
import { Meteor } from "meteor/meteor";
Meteor.methods({
async "tasks.delete"(taskId) {
// Throw Meteor.Error for user-facing errors
if (!this.userId) {
throw new Meteor.Error("not-authorized", "You must be logged in");
}
const task = await Tasks.findOneAsync(taskId);
if (!task) {
throw new Meteor.Error("not-found", "Task not found");
}
await Tasks.removeAsync(taskId);
},
});
Reactivity
Tracker.autorun
import { Tracker } from "meteor/tracker";
import { Tasks } from "./tasks";
// Reactive computation
const computation = Tracker.autorun(() => {
const count = Tasks.find().count();
console.log("Task count:", count);
});
// Stop computation
computation.stop();
ReactiveVar
import { ReactiveVar } from "meteor/reactive-var";
// Create reactive variable
const counter = new ReactiveVar(0);
// Get value
console.log(counter.get()); // 0
// Set value (triggers reactivity)
counter.set(counter.get() + 1);
// Use in Tracker
Tracker.autorun(() => {
console.log("Counter:", counter.get());
});
ReactiveDict
import { ReactiveDict } from "meteor/reactive-dict";
// Create reactive dictionary
const state = new ReactiveDict();
// Set values
state.set("filter", "active");
state.set("sortBy", "date");
// Get value
console.log(state.get("filter"));
// Set multiple
state.set({
filter: "completed",
sortBy: "priority",
});
// React to changes
Tracker.autorun(() => {
console.log("Filter:", state.get("filter"));
});
Session (Client)
import { Session } from "meteor/session";
// Set value
Session.set("selectedTask", taskId);
// Get value
const taskId = Session.get("selectedTask");
// Default value
Session.setDefault("pageSize", 10);
// React to changes
Tracker.autorun(() => {
const selected = Session.get("selectedTask");
console.log("Selected:", selected);
});
// Clear
Session.clear("selectedTask");
Custom Dependencies
import { Tracker } from "meteor/tracker";
class TaskCounter {
constructor() {
this.count = 0;
this.dep = new Tracker.Dependency();
}
increment() {
this.count++;
this.dep.changed(); // Trigger reactivity
}
getCount() {
this.dep.depend(); // Register dependency
return this.count;
}
}
const counter = new TaskCounter();
Tracker.autorun(() => {
console.log("Count:", counter.getCount());
});
counter.increment(); // Triggers autorun
Accounts System
Setup Accounts
# Install accounts packages
meteor add accounts-password
meteor add accounts-ui
User Creation
// Create user (async)
const userId = await Accounts.createUserAsync({
username: "john",
email: "john@example.com",
password: "password123",
profile: {
firstName: "John",
lastName: "Doe",
},
});
Login/Logout
// Login with password
await Meteor.loginWithPassword("john@example.com", "password123");
// Logout
await Meteor.logout();
// Current user
const user = Meteor.user();
const userId = Meteor.userId();
// Check if logged in
if (Meteor.userId()) {
console.log("User is logged in");
}
User Queries
// Get current user (reactive)
const user = Meteor.user();
// Access user data
console.log(user.username);
console.log(user.emails[0].address);
console.log(user.profile);
// Find users (server only)
const users = await Meteor.users
.find({
"profile.role": "admin",
})
.fetchAsync();
Password Management
// Change password
await Accounts.changePasswordAsync(oldPassword, newPassword);
// Reset password (send email)
await Accounts.forgotPassword({
email: "user@example.com",
});
// Reset with token
await Accounts.resetPassword(token, newPassword);
Email Verification
// Send verification email
Accounts.sendVerificationEmail(userId);
// Verify with token
await Accounts.verifyEmail(token);
// Check if verified
const user = Meteor.user();
if (user.emails[0].verified) {
console.log("Email verified");
}
Custom User Fields
// Server-side only
import { Accounts } from "meteor/accounts-base";
Accounts.onCreateUser((options, user) => {
user.profile = options.profile || {};
user.profile.credits = 100;
user.createdAt = new Date();
return user;
});
Login Hooks
// On login
Accounts.onLogin((details) => {
console.log("User logged in:", details.user);
});
// On logout
Accounts.onLogout((details) => {
console.log("User logged out");
});
// Validate login attempt
Accounts.validateLoginAttempt((attempt) => {
if (!attempt.allowed) {
return false;
}
// Custom validation
return true;
});
React Integration
Setup React
# Install React packages
meteor npm install react react-dom
meteor add react-meteor-data
useTracker Hook
import React from "react";
import { useTracker } from "meteor/react-meteor-data";
import { Tasks } from "../api/tasks";
function TaskList() {
const { tasks, loading, user } = useTracker(() => {
const handle = Meteor.subscribe("tasks");
return {
tasks: Tasks.find(
{},
{
sort: { createdAt: -1 },
},
).fetch(),
loading: !handle.ready(),
user: Meteor.user(),
};
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Tasks for {user?.username}</h1>
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.text}</li>
))}
</ul>
</div>
);
}
useFind Hook
import { useFind, useSubscribe } from "meteor/react-meteor-data";
import { Tasks } from "../api/tasks";
function TaskList() {
const isLoading = useSubscribe("tasks");
const tasks = useFind(() =>
Tasks.find(
{
done: false,
},
{
sort: { createdAt: -1 },
},
),
);
if (isLoading()) {
return <div>Loading...</div>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.text}</li>
))}
</ul>
);
}
Method Calls
import React, { useState } from "react";
import { Meteor } from "meteor/meteor";
function AddTask() {
const [text, setText] = useState("");
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setError(null);
try {
await Meteor.callAsync("tasks.insert", text);
setText("");
} catch (err) {
setError(err.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="New task"
/>
<button type="submit">Add</button>
{error && <p>{error}</p>}
</form>
);
}
User Authentication
import React from "react";
import { useTracker } from "meteor/react-meteor-data";
function LoginForm() {
const user = useTracker(() => Meteor.user());
const handleLogin = async (e) => {
e.preventDefault();
const email = e.target.email.value;
const password = e.target.password.value;
try {
await Meteor.loginWithPassword(email, password);
} catch (error) {
console.error(error);
}
};
const handleLogout = async () => {
await Meteor.logout();
};
if (user) {
return (
<div>
<p>Welcome, {user.username}!</p>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
return (
<form onSubmit={handleLogin}>
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
Blaze Templates
Template Basics
<!-- tasks.html -->
<template name="taskList">
<h1>Tasks</h1>
<ul>
{{#each tasks}}
<li>{{text}}</li>
{{/each}}
</ul>
</template>
// tasks.js
import { Template } from "meteor/templating";
import { Tasks } from "../api/tasks";
import "./tasks.html";
Template.taskList.helpers({
tasks() {
return Tasks.find(
{},
{
sort: { createdAt: -1 },
},
);
},
});
Template Events
Template.taskList.events({
"click .delete"(event, instance) {
Meteor.call("tasks.remove", this._id);
},
"submit .new-task"(event) {
event.preventDefault();
const text = event.target.text.value;
Meteor.call("tasks.insert", text);
event.target.text.value = "";
},
});
Template Lifecycle
Template.taskList.onCreated(function () {
// Initialize reactive variables
this.filter = new ReactiveVar("all");
// Subscribe to data
this.subscribe("tasks");
});
Template.taskList.onRendered(function () {
// DOM is ready
this.$(".task-input").focus();
});
Template.taskList.onDestroyed(function () {
// Cleanup
});
Template Instance
Template.taskList.helpers({
tasks() {
const instance = Template.instance();
const filter = instance.filter.get();
return Tasks.find({ status: filter });
},
});
Template.taskList.events({
"change .filter"(event, instance) {
instance.filter.set(event.target.value);
},
});
Common Packages
Core Packages
# Essential packages
meteor add mongo # MongoDB driver
meteor add reactive-var # Reactive variables
meteor add reactive-dict # Reactive dictionaries
meteor add tracker # Reactivity tracking
meteor add session # Client session state
# Accounts
meteor add accounts-password
meteor add accounts-ui
# Utilities
meteor add check # Type checking
meteor add random # Random ID generation
meteor add ejson # Extended JSON
Atmosphere Packages
# Routing
meteor add ostrio:flow-router-extra
# Collections
meteor add aldeed:collection2 # Schema validation
meteor add dburles:collection-helpers
# User management
meteor add alanning:roles # Role-based access
# Forms
meteor add aldeed:autoform # Automatic forms
# Testing
meteor add meteortesting:mocha
meteor add practicalmeteor:chai
NPM Packages
# Install via npm
meteor npm install bcrypt
meteor npm install moment
meteor npm install lodash
meteor npm install axios
Build & Deployment
Development
# Start dev server
meteor
# Specify port
meteor --port 3000
# Production mode
meteor --production
# Reset database
meteor reset
# MongoDB shell
meteor mongo
Build
# Build for deployment
meteor build ../output --architecture os.linux.x86_64
# Build as tarball
meteor build ../output --architecture os.linux.x86_64 --server-only
# Mobile builds
meteor build ../output --platforms ios,android
Environment Variables
# Settings file
meteor --settings settings.json
# MongoDB URL
MONGO_URL=mongodb://localhost:27017/myapp meteor
# Root URL
ROOT_URL=https://myapp.com meteor
# Port
PORT=3000 meteor
Settings File
{
"public": {
"apiUrl": "https://api.example.com",
"features": {
"betaMode": true
}
},
"private": {
"apiKey": "secret-key-123",
"smtp": {
"username": "user@example.com",
"password": "password"
}
}
}
// Access settings
console.log(Meteor.settings.public.apiUrl);
// Server only
if (Meteor.isServer) {
console.log(Meteor.settings.private.apiKey);
}
Deploy to Galaxy
# Deploy to Meteor Galaxy
DEPLOY_HOSTNAME=galaxy.meteor.com meteor deploy myapp.meteorapp.com --settings settings.json
# Free hosting (development only)
meteor deploy myapp.meteorapp.com
Security Best Practices
Remove Insecure Packages
# Remove insecure packages (allows all DB operations)
meteor remove insecure
meteor remove autopublish
After removing these, you must:
- Define publications for data access
- Use methods for data mutations
Validate Input
import { check, Match } from "meteor/check";
Meteor.methods({
async "tasks.insert"(text) {
// Always validate input
check(text, String);
if (text.length < 3) {
throw new Meteor.Error(
"invalid-input",
"Text must be at least 3 characters",
);
}
// Check authentication
if (!this.userId) {
throw new Meteor.Error("not-authorized");
}
await Tasks.insertAsync({
text,
owner: this.userId,
createdAt: new Date(),
});
},
});
Secure Publications
// BAD: Publishing sensitive data
Meteor.publish("users", function () {
return Meteor.users.find(); // Exposes passwords!
});
// GOOD: Filter fields
Meteor.publish("users", function () {
return Meteor.users.find(
{},
{
fields: {
username: 1,
profile: 1,
// Never publish services (contains passwords)
},
},
);
});
// GOOD: Check authorization
Meteor.publish("adminData", function () {
if (!this.userId || !isAdmin(this.userId)) {
return this.ready();
}
return AdminData.find();
});
Rate Limiting
import { DDPRateLimiter } from "meteor/ddp-rate-limiter";
// Limit method calls
DDPRateLimiter.addRule(
{
type: "method",
name: "tasks.insert",
connectionId() {
return true;
},
},
5,
1000,
); // 5 calls per second
// Limit by user
DDPRateLimiter.addRule(
{
type: "method",
name: "sendEmail",
userId(userId) {
return !!userId;
},
},
1,
60000,
); // 1 call per minute per user
Audit Arguments
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
Meteor.methods({
async "tasks.update"(taskId, updates) {
check(taskId, String);
check(updates, Object);
// Audit: only allow specific fields
const allowedFields = ["text", "done", "priority"];
Object.keys(updates).forEach((key) => {
if (!allowedFields.includes(key)) {
throw new Meteor.Error("invalid-field", `Field ${key} not allowed`);
}
});
await Tasks.updateAsync(taskId, {
$set: updates,
});
},
});
Browser Policy
# Install browser policy
meteor add browser-policy
// server/browser-policy.js
import { BrowserPolicy } from "meteor/browser-policy-common";
// Disallow all inline scripts
BrowserPolicy.content.disallowInlineScripts();
// Allow specific origins
BrowserPolicy.content.allowOriginForAll("https://cdn.example.com");
// CSP headers
BrowserPolicy.content.allowScriptOrigin("https://cdn.example.com");
Testing
Install Test Packages
# Mocha test framework
meteor add meteortesting:mocha
# Run tests
meteor test --driver-package meteortesting:mocha
# Full app tests
meteor test --full-app --driver-package meteortesting:mocha
Unit Tests
// imports/api/tasks.test.js
import { Meteor } from "meteor/meteor";
import { Random } from "meteor/random";
import { assert } from "chai";
import { Tasks } from "./tasks";
if (Meteor.isServer) {
describe("Tasks", () => {
describe("methods", () => {
it("can insert new task", async () => {
const taskId = await Meteor.callAsync("tasks.insert", "Test task");
const task = await Tasks.findOneAsync(taskId);
assert.equal(task.text, "Test task");
});
it("requires authentication", async () => {
try {
await Meteor.callAsync("tasks.insert", "Test");
assert.fail("Should have thrown error");
} catch (error) {
assert.equal(error.error, "not-authorized");
}
});
});
});
}
Mock User Context
import { Meteor } from "meteor/meteor";
import { Random } from "meteor/random";
// Create test user
const userId = Random.id();
const context = { userId };
// Call method with context
const methodInvocation = { userId };
const result = await Meteor.server.method_handlers["tasks.insert"].call(
methodInvocation,
"Test task",
);
Integration Tests
import { resetDatabase } from "meteor/xolvio:cleaner";
describe("Tasks integration", () => {
beforeEach(() => {
resetDatabase();
});
it("creates and finds task", async () => {
await Tasks.insertAsync({ text: "Test" });
const count = await Tasks.find().countAsync();
assert.equal(count, 1);
});
});
Examples
Complete CRUD Example
// imports/api/tasks/tasks.js
import { Mongo } from "meteor/mongo";
export const Tasks = new Mongo.Collection("tasks");
// imports/api/tasks/methods.js
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
import { Tasks } from "./tasks";
Meteor.methods({
async "tasks.insert"(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error("not-authorized");
}
return await Tasks.insertAsync({
text,
done: false,
owner: this.userId,
createdAt: new Date(),
});
},
async "tasks.update"(taskId, done) {
check(taskId, String);
check(done, Boolean);
const task = await Tasks.findOneAsync(taskId);
if (task.owner !== this.userId) {
throw new Meteor.Error("not-authorized");
}
await Tasks.updateAsync(taskId, {
$set: { done },
});
},
async "tasks.remove"(taskId) {
check(taskId, String);
const task = await Tasks.findOneAsync(taskId);
if (task.owner !== this.userId) {
throw new Meteor.Error("not-authorized");
}
await Tasks.removeAsync(taskId);
},
});
// imports/api/tasks/publications.js
import { Meteor } from "meteor/meteor";
import { Tasks } from "./tasks";
Meteor.publish("tasks", function () {
if (!this.userId) {
return this.ready();
}
return Tasks.find(
{
owner: this.userId,
},
{
sort: { createdAt: -1 },
},
);
});
React Component Example
// imports/ui/TaskList.jsx
import React, { useState } from "react";
import { useTracker } from "meteor/react-meteor-data";
import { Tasks } from "../api/tasks/tasks";
export function TaskList() {
const [newTask, setNewTask] = useState("");
const { tasks, loading } = useTracker(() => {
const handle = Meteor.subscribe("tasks");
return {
tasks: Tasks.find(
{},
{
sort: { createdAt: -1 },
},
).fetch(),
loading: !handle.ready(),
};
});
const handleSubmit = async (e) => {
e.preventDefault();
if (!newTask.trim()) return;
try {
await Meteor.callAsync("tasks.insert", newTask);
setNewTask("");
} catch (error) {
alert(error.message);
}
};
const handleToggle = async (taskId, done) => {
await Meteor.callAsync("tasks.update", taskId, !done);
};
const handleDelete = async (taskId) => {
await Meteor.callAsync("tasks.remove", taskId);
};
if (loading) {
return <div>Loading tasks...</div>;
}
return (
<div>
<h1>My Tasks</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add Task</button>
</form>
<ul>
{tasks.map((task) => (
<li key={task._id}>
<input
type="checkbox"
checked={task.done}
onChange={() => handleToggle(task._id, task.done)}
/>
<span
style={{
textDecoration: task.done ? "line-through" : "none",
}}
>
{task.text}
</span>
<button onClick={() => handleDelete(task._id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Authenticated Route Example
// imports/ui/App.jsx
import React from "react";
import { useTracker } from "meteor/react-meteor-data";
import { TaskList } from "./TaskList";
import { LoginForm } from "./LoginForm";
export function App() {
const user = useTracker(() => Meteor.user());
if (!user) {
return <LoginForm />;
}
return (
<div>
<header>
<h1>Welcome, {user.username}!</h1>
<button onClick={() => Meteor.logout()}>Logout</button>
</header>
<TaskList />
</div>
);
}
Gotchas
Async Collection Methods
In Meteor 3.x, all collection methods are async:
// WRONG (Meteor 2.x)
const task = Tasks.findOne({ _id: id });
Tasks.insert({ text: "New" });
// CORRECT (Meteor 3.x)
const task = await Tasks.findOneAsync({ _id: id });
await Tasks.insertAsync({ text: "New" });
Fetch Results from Find
find() returns a cursor, not an array:
// Get cursor
const cursor = Tasks.find({ done: false });
// To get array, use fetchAsync()
const tasks = await cursor.fetchAsync();
// Or use .fetch() in reactive contexts
const tasks = Tasks.find().fetch();
Parameters as Promises
In Next.js-style routing (Meteor 3.x):
// WRONG
export default function Page({ params }) {
const { slug } = params; // Error!
}
// CORRECT
export default async function Page({ params }) {
const { slug } = await params;
}
Subscriptions Ready
Always check if subscription is ready:
const { data, loading } = useTracker(() => {
const handle = Meteor.subscribe("tasks");
// Don't query until subscription is ready
if (!handle.ready()) {
return { data: [], loading: true };
}
return {
data: Tasks.find().fetch(),
loading: false,
};
});
Method Context
this.userId is only available in methods and publications:
// WRONG: helpers
Template.myTemplate.helpers({
isOwner() {
return this.userId === Meteor.userId(); // this.userId undefined
},
});
// CORRECT: use Meteor.userId()
Template.myTemplate.helpers({
isOwner() {
return this.owner === Meteor.userId();
},
});
Reactivity in Methods
Methods are NOT reactive:
// WRONG: This won't update reactively
Meteor.methods({
"tasks.count"() {
return Tasks.find().count(); // Not reactive!
},
});
// CORRECT: Use publications for reactive data
Meteor.publish("taskCount", function () {
let count = Tasks.find().count();
this.added("counts", "tasks", { count });
const handle = Tasks.find().observeChanges({
added: () => {
count++;
this.changed("counts", "tasks", { count });
},
removed: () => {
count--;
this.changed("counts", "tasks", { count });
},
});
this.ready();
this.onStop(() => handle.stop());
});
File Structure Loading
Meteor loads files in specific order:
lib/directories (deepest first)server/directoriesclient/directories- Everything else
- Files in subdirectories before parent
main.*files last
Use import/export instead of relying on load order.
MongoDB _id
Meteor uses custom IDs by default:
// Default: Random string ID
const id = Tasks.insertAsync({ text: "Task" });
// Returns: "xY2zAb3cD4eF5g6h"
// Use MongoDB ObjectID
import { MongoInternals } from "meteor/mongo";
const id = Tasks.insertAsync({
_id: new MongoInternals.NpmModule.ObjectId(),
text: "Task",
});
Also see
- Meteor Official Documentation - Complete guide
- Meteor Guide - Best practices
- Meteor API Reference - Full API documentation
- Meteor Forums - Community support
- Atmosphere Packages - Package repository
- Meteor GitHub - Source code
- Meteor 3.0 Migration Guide - Upgrade guide