NexusCS

Meteor.js

JavaScript
Meteor is a full-stack JavaScript platform for developing modern web and mobile applications. It includes a key set of technologies for building connected-client reactive applications.
fullstack
javascript
mongodb
reactive

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:

  1. lib/ directories (deepest first)
  2. server/ directories
  3. client/ directories
  4. Everything else
  5. Files in subdirectories before parent
  6. 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