# How to Add a New Tauri Command in Meetily's Rust Backend

> Learn how to add a new Tauri command to Meetily's Rust backend. Implement async functions, register them in tauri::generate_handler!, and invoke them from your frontend.

- Repository: [Zackriya Solutions/meetily](https://github.com/Zackriya-Solutions/meetily)
- Tags: how-to-guide
- Published: 2026-07-20

---

**To add a new Tauri command in Meetily, implement an async Rust function annotated with `#[tauri::command]` in a module like [`frontend/src-tauri/src/api/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/api/commands.rs), then register it in the `tauri::generate_handler!` macro inside [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) (around line 526), and finally invoke it from the TypeScript frontend using `invoke('command_name')`.**

Meetily is an open-source meeting assistant built with Tauri, combining a Rust backend with a web-based frontend. Adding custom backend functionality requires exposing new commands through Tauri's IPC system that bridge the frontend and Rust layers. This guide walks through the exact implementation pattern used in the `Zackriya-Solutions/meetily` repository, referencing specific source files and line numbers.

## Understanding Meetily's Tauri Command Architecture

Tauri commands in Meetily are regular Rust async functions marked with the `#[tauri::command]` attribute. They reside in domain-specific modules such as [`frontend/src-tauri/src/api/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/api/commands.rs) for generic operations or [`frontend/src-tauri/src/audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_commands.rs) for audio-specific functionality. 

The global registration happens in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) via the `tauri::generate_handler!` macro (found around lines 526-540). This macro collects all exposed function pointers and builds the IPC dispatcher that routes `invoke` calls from the JavaScript frontend to the appropriate Rust handler. Existing commands like `start_recording`, `stop_recording`, and `is_recording` follow this exact pattern, making them ideal reference implementations.

## Step-by-Step Implementation Guide

### 1. Implement the Command Function

Create or open a logical module under `frontend/src-tauri/src/`. For generic API utilities, use [`api/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/api/commands.rs); for feature-specific logic, follow the pattern in [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs). 

Define a `pub async fn` with the `#[tauri::command]` attribute. Accept `AppHandle<R>` (where `R: Runtime`) if you need access to Tauri state or event emitters. Return `Result<T, String>` to allow proper error handling in the frontend.

```rust
use tauri::{AppHandle, Runtime};

#[tauri::command]
pub async fn ping<R: Runtime>(_app: AppHandle<R>) -> Result<String, String> {
    Ok("pong".to_string())
}

```

### 2. Register the Command in lib.rs

Open [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) and locate the `invoke_handler` block around line 526. Add your function identifier to the `tauri::generate_handler!` list, maintaining the existing comma separation.

```rust
.invoke_handler(tauri::generate_handler![
    start_recording,
    stop_recording,
    is_recording,
    // ... existing commands ...
    ping,  // newly added command
])

```

The `generate_handler!` macro expands this list into the routing table used by the Tauri runtime to dispatch IPC calls.

### 3. Invoke from the Frontend

Call the command from your TypeScript code using the `invoke` function from `@tauri-apps/api/tauri`. Pass the command name as the first argument and an object containing any arguments as the second.

```typescript
import { invoke } from '@tauri-apps/api/tauri';

async function testBackend() {
  try {
    const response = await invoke<string>('ping');
    console.log(response); // Outputs: "pong"
  } catch (error) {
    console.error('Command failed:', error);
  }
}

```

## Complete Working Example: The Ping Command

Here is the full implementation of a health-check command that returns `"pong"`, following the patterns found in the Meetily codebase.

**File:** [`frontend/src-tauri/src/api/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/api/commands.rs)

```rust
use tauri::{AppHandle, Runtime};

/// Simple health-check endpoint demonstrating the command pattern.
/// Mirrors the structure of start_recording in lib.rs (lines 83-108).
#[tauri::command]
pub async fn ping<R: Runtime>(_app: AppHandle<R>) -> Result<String, String> {
    // In production commands, you might access state or emit events here
    Ok("pong".to_string())
}

```

**Update in:** [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) (line ~526)

```rust
    .invoke_handler(tauri::generate_handler![
        start_recording,
        stop_recording,
        is_recording,
        ping,  // Add this line
    ])

```

**Frontend usage:** Any TypeScript component

```typescript
import { invoke } from '@tauri-apps/api/tauri';

const checkBackendHealth = async () => {
  const reply = await invoke<string>('ping');
  return reply === 'pong';
};

```

## Advanced Command Patterns

### Handling Command Arguments

Commands can accept serializable arguments from the frontend. Define parameters after the `AppHandle` (if present). Meetily's `set_meeting_name` pattern demonstrates storing frontend data in backend state.

```rust
#[tauri::command]
pub async fn set_meeting_name<R: Runtime>(
    app: AppHandle<R>, 
    name: String,
    meeting_id: u64
) -> Result<(), String> {
    // Access shared state or database via app.state::<YourState>()
    Ok(())
}

```

Call with:

```typescript
await invoke('set_meeting_name', { name: 'Team Sync', meetingId: 123 });

```

### Emitting Events to the Frontend

Use `app.emit()` to send real-time updates without waiting for a return value. This pattern appears in Meetily's recording commands for progress updates.

```rust
#[tauri::command]
pub async fn process_data<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
    // Perform work...
    
    app.emit("processing-complete", serde_json::json!({ 
        "status": "success",
        "timestamp": chrono::Utc::now().to_rfc3339()
    }))
    .map_err(|e| e.to_string())
}

```

Listen in TypeScript:

```typescript
import { listen } from '@tauri-apps/api/event';

listen('processing-complete', (event) => {
  console.log('Backend event:', event.payload);
});

```

### Accessing Shared State

Access the application state defined in [`frontend/src-tauri/src/state.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/state.rs) using `app.state::<T>()`. This allows commands to interact with global resources like the recording session or database connections.

```rust
use crate::state::MeetingState;

#[tauri::command]
pub async fn get_current_meeting<R: Runtime>(
    app: AppHandle<R>
) -> Result<String, String> {
    let state = app.state::<MeetingState>();
    state.get_active_meeting()
        .map_err(|e| e.to_string())
}

```

## Summary

- **Implement** a `pub async fn` with the `#[tauri::command]` attribute in the appropriate module under `frontend/src-tauri/src/`, using `AppHandle<R>` for state access and returning `Result<T, String>`.
- **Register** the function name in the `tauri::generate_handler!` macro inside [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) (around line 526) to expose it to the frontend.
- **Invoke** the command from TypeScript using `invoke('command_name', { args })` from `@tauri-apps/api/tauri`, handling the returned Promise for results or errors.

## Frequently Asked Questions

### Where should I place new Tauri commands in the Meetily codebase?

Place commands in domain-specific modules under `frontend/src-tauri/src/`. Use [`api/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/api/commands.rs) for general utilities, create [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs) for recording features, or add to existing feature modules. Ensure the module is declared in [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs) if it's new, and that functions are marked `pub` so they're visible to the `generate_handler!` macro.

### How do I access application state from within a command?

Import `AppHandle<R>` from `tauri` as the first parameter to your command function, then call `app.state::<YourStateType>()` where `YourStateType` is defined in [`state.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/state.rs). For example, `app.state::<MeetingState>()` gives you access to shared meeting data. This follows the same pattern used by `start_recording` in [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs) lines 119-131.

### Can I return custom error types instead of String?

Yes, any type implementing `std::fmt::Display` or `serde::Serialize` works, but `String` errors serialize cleanly to JavaScript Error objects. For custom error types, implement `From<YourError> for String` or use `map_err(|e| e.to_string())` before returning. The frontend receives these as rejected Promise values.

### Why is my new command returning "command not found" in the frontend?

This occurs when the function isn't registered in the `generate_handler!` list in [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs). Verify the function name appears in the macro invocation (around line 526), that it's marked `pub`, and that you've rebuilt the application with [`./clean_run.sh`](https://github.com/Zackriya-Solutions/meetily/blob/main/./clean_run.sh) or `cargo build`. Changes to Rust code require recompilation before the frontend can discover new commands.