Creating Your First Rust Command
Learn how to create and register Tauri commands - the core mechanism that lets your React frontend call Rust functions with type safety
A Tauri command is a Rust function exposed to your frontend through an annotation and a registration step. It is the fundamental bridge between the JavaScript running in the WebView and the Rust backend that has access to the file system, system APIs, and native performance. Without commands, your Tauri app is just a website in a window.
Commands solve a real architectural problem: the WebView sandbox cannot touch the operating system directly. The browser security model prevents JavaScript from reading files, spawning processes, or accessing hardware. Tauri's command system punches a controlled, type-safe hole through that sandbox — each command is an explicit doorway you build, and Rust guards what passes through it.
How Commands Work
A command is an ordinary Rust function with a #[tauri::command] attribute placed above it. The attribute tells Tauri's macro system to generate the serialization glue that converts JavaScript arguments into Rust types and Rust return values back into JavaScript promises.
When your frontend calls invoke('command_name', { args }), Tauri serializes the arguments as JSON, passes them across the inter-process communication (IPC) boundary to the Rust side, deserializes them into the function's parameter types, runs the function, serializes the return value back to JSON, and resolves the JavaScript promise with the result.
The mental model for a beginner: think of a command like an API endpoint, but instead of going over HTTP to a remote server, it goes over IPC to Rust code running in the same process. The function signature defines the contract — what arguments it accepts and what shape of data it returns.
Anatomy of a Command
A Tauri command has three parts that must all be present. Missing any one of them breaks the connection.
First, the function itself with the attribute:
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}! You've been greeted from Rust.", name)
}
Second, the registration inside the builder in your run function:
.invoke_handler(tauri::generate_handler![greet])
Third, the frontend call site (covered in detail when calling Rust from React), which invokes the command by its function name:
import { invoke } from '@tauri-apps/api/core';
const message = await invoke<string>('greet', { name: 'Alice' });
Command Names Are Function Names:
The command name used in invoke() is the Rust function name exactly as written, not a string you configure separately. If your Rust function is get_user_data, the frontend calls invoke('get_user_data', ...). There is no renaming mechanism — rename the function if you need a different command name. Snake_case on the Rust side maps directly to the string passed to invoke.
Creating Your First Command
The process of adding a command to a Tauri project follows a fixed sequence. Each step builds on the previous one.
Step 1: Open lib.rs and locate the run function
Every Tauri v2 project has a src-tauri/src/lib.rs file. This file contains a run function that builds and launches the Tauri application. You will add your command function to this file (or a module it references) and register it inside the builder chain.
Open src-tauri/src/lib.rs. The run function typically looks like this in a fresh Tauri v2 project:
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The builder chain is where all configuration happens. The invoke_handler call you will add goes between Builder::default() and .run(...).
Step 2: Write the command function
Add a function annotated with #[tauri::command] above the run function. Start with the simplest possible command — one that takes no arguments and returns a string.
// src-tauri/src/lib.rs
#[tauri::command]
fn greet() -> String {
"Hello from Rust!".to_string()
}
The #[tauri::command] attribute does the heavy lifting. At compile time, Tauri's macro generates the code that handles argument deserialization, return value serialization, and integration with the IPC layer. You write a plain Rust function; the macro makes it callable from JavaScript.
Where to Place the Function:
The command function must be in scope where generate_handler! is called. If you define it in lib.rs directly, it is in scope automatically. If you define it in a separate module, you must import the module and prefix the function name with the module path in the macro (e.g., commands::greet).
Step 3: Register the command with invoke_handler
The generate_handler! macro takes a comma-separated list of function names and wires them into Tauri's command registry. Add it to the builder chain with .invoke_handler(...).
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The complete lib.rs now looks like this:
// src-tauri/src/lib.rs
#[tauri::command]
fn greet() -> String {
"Hello from Rust!".to_string()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Step 4: Build and verify
Run npm run tauri dev from your project root. The Rust code compiles, and if the command is registered correctly, Tauri will include it in the generated bindings. There is no visible output from the command itself until the frontend calls it — but a successful compilation confirms the registration is valid.
Compilation Confirms Registration:
If tauri dev compiles without errors, your command is registered correctly. Tauri's macro system validates at compile time that every function listed in generate_handler! exists, is annotated with #[tauri::command], and has a signature compatible with the IPC layer. A clean build is your proof that the wiring is complete.
Defining Commands in a Separate Module
As your application grows, putting every command in lib.rs becomes unwieldy. Tauri v2 supports organizing commands into dedicated modules — a pattern that keeps lib.rs clean and groups related functionality together.
Create a file at src-tauri/src/commands.rs:
// src-tauri/src/commands.rs
#[tauri::command]
pub fn greet() -> String {
"Hello from Rust!".to_string()
}
#[tauri::command]
pub fn get_app_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
Then declare the module in lib.rs and register the commands with their full paths:
// src-tauri/src/lib.rs
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::greet,
commands::get_app_version
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The commands:: prefix in generate_handler! is the Rust module path — it tells the macro where to find the function. The command name that the frontend uses is still just greet or get_app_version. The module prefix is stripped from the command name automatically.
Missing Module Declaration Breaks Registration:
If you create commands.rs but forget to add mod commands; at the top of lib.rs, the Rust compiler will report that the module cannot be found. The mod declaration is what tells Rust the file exists and makes its contents available. Without it, generate_handler! cannot resolve commands::greet, and the build fails.
Returning Data from Commands
Commands can return any type that implements serde::Serialize. For simple cases, Rust's standard types work out of the box.
A command returning a string:
#[tauri::command]
fn get_message() -> String {
"Hello from Rust!".to_string()
}
A command returning a number:
#[tauri::command]
fn get_answer() -> i32 {
42
}
For structured data, define a struct and derive Serialize:
// src-tauri/src/lib.rs
use serde::Serialize;
#[derive(Serialize)]
struct UserInfo {
username: String,
email: String,
active: bool,
}
#[tauri::command]
fn get_user() -> UserInfo {
UserInfo {
username: "alice".to_string(),
email: "alice@example.com".to_string(),
active: true,
}
}
The serialization happens automatically. When the frontend calls invoke('get_user'), the promise resolves with a JavaScript object: { username: "alice", email: "alice@example.com", active: true }.
Serialization Is Implicit But Required:
Every type that crosses the IPC boundary must implement Serialize (for return values) or Deserialize (for arguments). If you forget to derive the trait, the Rust compiler produces an error pointing at the command function. The error messages mention Serialize or Deserialize not being satisfied — that is your cue to add the derive macro or implement the trait manually.
The Complete File After Adding a Command
Here is the full lib.rs after creating and registering a command, with the module organization approach shown as well for reference:
// src-tauri/src/lib.rs
use serde::Serialize;
mod commands;
#[derive(Serialize)]
struct UserInfo {
username: String,
email: String,
active: bool,
}
#[tauri::command]
fn get_user() -> UserInfo {
UserInfo {
username: "alice".to_string(),
email: "alice@example.com".to_string(),
active: true,
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_user,
commands::greet,
commands::get_app_version
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
And the corresponding commands.rs:
// src-tauri/src/commands.rs
#[tauri::command]
pub fn greet() -> String {
"Hello from Rust!".to_string()
}
#[tauri::command]
pub fn get_app_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
Common Mistakes
Beginners encounter a small set of predictable errors when creating their first command. Knowing them in advance saves debugging time.
Forgetting to register the command in generate_handler! is the most frequent mistake. The function compiles fine because it is valid Rust, but calling it from the frontend produces an error: command not found. The compiler does not warn about unregistered commands — the macro only knows about the names you give it.
Mismatched argument names between Rust and JavaScript. Rust uses snake_case (user_name), and Tauri expects the frontend to pass arguments with camelCase keys (userName). The serialization layer handles this conversion automatically, but only if the struct fields or function parameter names follow Rust conventions. If you name a parameter username (all lowercase, no underscore), the frontend must also use username — there is no conversion because the name has no word boundary to transform.
Using types that do not implement Serialize or Deserialize. Standard library types like String, i32, bool, Vec<T>, and Option<T> all implement these traits and work immediately. Custom types need the derive macros. Third-party types from crates may or may not implement them — check the crate documentation.