# How to Register a New Tauri Command in lib.rs and Call It from the Next.js Frontend

> Learn to register a new Tauri command in lib.rs and call it from your Next.js frontend. Follow our simple steps to integrate Rust functions with your web app.

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

---

**To register a new Tauri command in Meetily, annotate a Rust function with `#[tauri::command]` in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs), add it to the `tauri::generate_handler!` macro, and invoke it from the frontend using the `invoke` function from `@tauri-apps/api/core`.**

Meetily bridges its Rust backend with the Next.js frontend using Tauri’s command system. This pattern allows you to expose native capabilities—such as file system access, audio recording, or custom business logic—to the React UI while maintaining type safety and async performance. Below is the complete workflow for registering a new command and consuming it from the frontend.

## Step 1: Define the Tauri Command in Rust

Commands reside in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) as asynchronous functions marked with the `#[tauri::command]` attribute.

### Command Signature and Return Types

The standard pattern uses generic `Runtime` and returns `Result<T, String>` to handle errors gracefully:

```rust
// File: frontend/src-tauri/src/lib.rs
#[tauri::command]
pub async fn greet<R: Runtime>(name: String) -> Result<String, String> {
    // Perform async work here: file I/O, database queries, etc.
    Ok(format!("Hello, {}!", name))
}

```

- **`R: Runtime`** – Enables access to Tauri’s runtime capabilities and ensures compatibility with multi-platform builds.
- **`pub` visibility** – The function must be public so the handler can reference it during macro expansion.
- **`Result<String, String>`** – On success, the `Ok` variant serializes to the frontend; on failure, the `Err` string propagates as a rejected JavaScript Promise.

### Accessing the AppHandle

When you need to interact with Tauri’s API (e.g., opening dialogs, managing windows, or accessing app directories), accept `AppHandle<R>` as the first parameter:

```rust
#[tauri::command]
pub async fn open_models_folder<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
    app.shell()
        .open(app.path().app_data_dir().unwrap().join("models"))
        .map_err(|e| e.to_string())
}

```

## Step 2: Register the Command in the Tauri Builder

Locate the `invoke_handler` section in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) (approximately lines 525–560). Add your command identifier to the `tauri::generate_handler!` macro:

```rust
// File: frontend/src-tauri/src/lib.rs
pub fn run() {
    let mut builder = tauri::Builder::default();
    builder
        .plugin(tauri_plugin_notification::init())
        // ... other plugins ...
        .invoke_handler(tauri::generate_handler![
            start_recording,
            stop_recording,
            // ... existing commands ...
            greet,                // <-- newly added command
        ])
        .build(tauri::generate_context!())
        .expect("error while building tauri application")
        .run(|_app_handle, _event| { /* ... */ });
}

```

The macro generates the routing glue that maps frontend `invoke` calls to the corresponding Rust functions. Each identifier in the list must match the function name exactly.

## Step 3: Invoke the Command from the Next.js Frontend

Import the `invoke` helper from `@tauri-apps/api/core` and call it with the command name and a payload object:

```tsx
// Example: frontend/src/components/Example.tsx
import { invoke } from '@tauri-apps/api/core';
import { useState } from 'react';

export default function Example() {
  const [message, setMessage] = useState<string>('');

  async function sayHello() {
    try {
      // Generic <string> matches the Ok return type in Rust
      const result = await invoke<string>('greet', { name: 'Alice' });
      setMessage(result);  // "Hello, Alice!"
    } catch (error) {
      console.error('Command failed:', error);
    }
  }

  return (
    <div>
      <button onClick={sayHello}>Greet</button>
      {message && <p>{message}</p>}
    </div>
  );
}

```

- **Argument mapping** – The payload object keys (`name`) must match the Rust parameter names.
- **Type safety** – Use the generic parameter (`invoke<string>`) to align with the Rust return type.
- **Error handling** – Rust `Err` variants reject the Promise, so wrap calls in `try/catch` blocks.

## Organizing Commands in Separate Modules

For larger features, move commands to dedicated modules (e.g., [`src/api/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/src/api/commands.rs)) and re-export them in [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs):

```rust
// File: frontend/src-tauri/src/api/commands.rs
#[tauri::command]
pub async fn get_app_version() -> Result<String, String> {
    Ok(env!("CARGO_PKG_VERSION").to_string())
}

```

Then in [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs):

```rust
mod api;                    // Bring module into scope
pub use api::commands::*;   // Re-export public symbols

// In the invoke_handler:
.invoke_handler(tauri::generate_handler![
    // ... other commands ...
    get_app_version,
])

```

This pattern keeps [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs) clean and groups related functionality by domain (audio, analytics, file system, etc.).

## Debugging and Error Handling

**Rust side** – Run the application with `RUST_LOG=debug ./clean_run.sh` to see `log::info!` output and trace command execution flow.

**Frontend side** – All commands return Promises. Always implement `.catch()` or `try/catch` blocks to handle Rust errors gracefully in the UI.

**Command not found errors** – If the frontend reports "command not found," verify:
1. The function is marked `#[tauri::command]`
2. The identifier is listed in `tauri::generate_handler!`
3. The Tauri application has been rebuilt (`cargo build`) to include the new handler

## Summary

- **Define** – Create an `async fn` with `#[tauri::command]` in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs), using `Result<T, String>` for error propagation.
- **Register** – Add the function name to the `tauri::generate_handler!` list inside the builder configuration (around line 525 in Meetily’s [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs)).
- **Invoke** – Call `invoke<T>('commandName', { args })` from any Next.js component after importing from `@tauri-apps/api/core`.
- **Scale** – Move complex commands to submodules (e.g., [`src/audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/src/audio/recording_commands.rs)) and re-export them to keep the codebase maintainable.

## Frequently Asked Questions

### Why must the Rust function be public (pub)?

The `tauri::generate_handler!` macro generates static references to your functions during compilation. If the command is private to its module, the macro cannot generate the necessary FFI bridges to expose it to the JavaScript runtime. Marking the function `pub` ensures visibility across the crate boundary.

### Can I use synchronous functions instead of async?

Yes, Tauri supports synchronous commands, but **async is recommended** for Meetily to prevent blocking the main thread during I/O operations (file writes, network requests, or audio processing). If you omit `async`, simply remove the `.await` on the frontend; however, the return type should still be `Result<T, String>` for consistency.

### How do I pass complex objects between the frontend and backend?

Serialize complex data using JSON-compatible structures. On the Rust side, derive `serde::Deserialize` for input arguments and `serde::Serialize` for return types. On the frontend, pass plain JavaScript objects—the `invoke` function handles serialization automatically. Ensure field names match exactly between TypeScript interfaces and Rust structs.

### Where should I place command logic for a large codebase?

For production-scale code, avoid bloating [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs). Instead, create domain-specific modules (e.g., [`src/audio/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/src/audio/commands.rs), [`src/fs/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/src/fs/commands.rs)) and import them into [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs) with `mod` and `pub use`. This mirrors Meetily’s existing pattern where recording commands live in [`frontend/src-tauri/src/audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_commands.rs) and are re-exported for registration in the main handler.