Accessing Resources
Learn how to locate and read bundled resource files from both Rust and React with platform-specific handling in Tauri v2.
Once you’ve added files to your Tauri bundle using the resources configuration, those files get embedded into your application package. At runtime, you need a way to find them and read their contents — whether from Rust commands or from your React frontend. The Path API is what resolves those locations.
Where Resource Files Live at Runtime
Tauri places all bundled resource files into a single resource directory. The exact location on disk depends on the operating system, but you never need to calculate the path yourself. Instead, you resolve a resource path relative to that directory using a special base path.
The system constant is BaseDirectory::Resource. Any path you resolve against it will be joined to the actual resource directory. For example, if you bundled a file as lang/en.json, resolving lang/en.json against BaseDirectory::Resource will give you the full, real path to that file at runtime.
Paths you pass to the resolution API follow the same rules as the source paths in tauri.conf.json > bundle > resources:
"folder/file.txt"→$RESOURCE/folder/file.txt"../relative/folder/file.toml"→$RESOURCE/_up_/relative/folder/file.toml"/absolute/path/file.txt"→$RESOURCE/_root_/absolute/path/file.txt
This consistency means you can copy the string directly from your configuration into the resolve call without mental translation.
Not a regular file path on Android:
On Android, resource files live inside the APK and are accessed through a special asset://localhost/ URI. The resolution API still works the same way, but the returned string is a URI, not a normal file system path. We cover this in the platform section below.
Resolving a Resource Path in Rust
On the Rust side, you need a PathResolver instance. You can obtain it from App inside the setup hook, or from AppHandle inside any command.
// src-tauri/src/lib.rs
use tauri::Manager;
use tauri::path::BaseDirectory;
#[tauri::command]
fn load_greeting(handle: tauri::AppHandle) -> Result<String, String> {
let resource_path = handle
.path()
.resolve("lang/en.json", BaseDirectory::Resource)
.map_err(|e| e.to_string())?;
let content = std::fs::read_to_string(&resource_path)
.map_err(|e| format!("failed to read {}: {}", resource_path.display(), e))?;
Ok(content)
}
The call handle.path().resolve("lang/en.json", BaseDirectory::Resource) produces a platform-specific path. On desktop it’s an ordinary file path — you can use std::fs directly. On Android, the result is a URI, and std::fs will not work.
If you need to resolve a path before the app is fully built (for example, to register database migrations), do it inside the .setup() closure, which gives you an App instance:
tauri::Builder::default()
.setup(|app| {
let resource_path = app
.path()
.resolve("migrations", BaseDirectory::Resource)
.expect("failed to resolve resource path");
let migrations = load_migrations(&resource_path);
app.handle().plugin(
tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:test.db", migrations)
.build(),
)?;
Ok(())
})
Don't resolve before you have an AppHandle:
There is no global way to get a resource path before the Tauri app starts. Code that runs outside setup or a command has no access to the path resolver. Move such initialization into the setup hook.
Resolving a Resource Path in JavaScript
On the frontend, use resolveResource from @tauri-apps/api/path. This function returns the same fully resolved path that Rust gives you — as a string.
import { resolveResource } from '@tauri-apps/api/path';
const resourcePath = await resolveResource('lang/en.json');
console.log(resourcePath);
// on Windows: C:\Users\...\AppData\Local\com.myapp.app\lang\en.json
// on Android: asset://localhost/lang/en.json
The JavaScript API handles the platform difference transparently. You don’t need to check whether the path is a URI or a file path.
Once you have the resolved path, the next step is to actually read the file’s content. The most common approach is to pass the path to a Rust command that performs the read operation:
// src/App.tsx
import { invoke } from '@tauri-apps/api/core';
import { resolveResource } from '@tauri-apps/api/path';
const path = await resolveResource('lang/en.json');
const content = await invoke<string>('read_resource', { path });
The corresponding Rust command would look like this:
#[tauri::command]
fn read_resource(path: String) -> Result<String, String> {
std::fs::read_to_string(&path)
.map_err(|e| e.to_string())
}
Loading Resource Files on Desktop vs Android
Because std::fs cannot read from the Android asset URI, you must use the file system plugin on mobile. The resolution is the same; only the reading mechanism changes.
#[tauri::command]
fn load_config(handle: tauri::AppHandle) -> Result<String, String> {
let resource_path = handle
.path()
.resolve("config.json", BaseDirectory::Resource)
.map_err(|e| e.to_string())?;
std::fs::read_to_string(&resource_path)
.map_err(|e| e.to_string())
}
Silent failure on Android:
If you use std::fs::read_to_string with the Android asset URI, it will fail with a PermissionDenied or NotFound error at runtime. There is no compile-time warning. Always use the fs plugin’s read_to_string method when targeting Android.
Displaying a Bundled Image in the Frontend
For binary media files — images, audio, video — you typically want to use the resource directly in the webview’s DOM. To do that, convert the resource path into an asset protocol URL that the webview can load.
import { resolveResource } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/core';
const resourcePath = await resolveResource('images/logo.png');
const assetUrl = convertFileSrc(resourcePath);
// assetUrl is now something like http://asset.localhost/... that works in <img src=...>
You can then use assetUrl as the src attribute in an <img> tag or pass it to any component that expects a URL.
For this to work, the security configuration must allow the asset protocol to access the resource directory. In your capability file (src-tauri/capabilities/default.json), make sure you include a scope that covers your resources:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
{
"identifier": "asset:default",
"allow": [{ "path": "$RESOURCE/**" }]
}
]
}
If you store resources elsewhere, widen the scope accordingly. The $RESOURCE variable resolves to the same base directory used by BaseDirectory::Resource.
The asset protocol scope is separate:
This permission is independent from the fs plugin scope. Even if you never use the file system plugin, you must grant asset protocol access to serve resources in the webview.
Complete Example — A Greeting App with Language Files
The most common pattern for resource access is loading configuration or language files that were bundled at build time. Here’s a small, complete example that bundles two JSON language files and exposes a greeting in the selected language.
Step 1 — Add the files to your source tree:
src-tauri/
├── lang/
│ ├── en.json
│ └── de.json
└── ...
lang/en.json:
{ "greeting": "Hello!" }
lang/de.json:
{ "greeting": "Guten Tag!" }
Step 2 — Declare them as resources in tauri.conf.json:
{
"bundle": {
"resources": ["lang/*"]
}
}
Step 3 — Write a Rust command that resolves the path, reads the file, and returns the greeting:
// src-tauri/src/lib.rs
use tauri::Manager;
use tauri::path::BaseDirectory;
use serde_json::Value;
#[tauri::command]
fn greet(handle: tauri::AppHandle, language: String) -> Result<String, String> {
let file_name = format!("lang/{}.json", language);
let resource_path = handle
.path()
.resolve(&file_name, BaseDirectory::Resource)
.map_err(|e| format!("cannot resolve {}: {}", file_name, e))?;
let content = std::fs::read_to_string(&resource_path)
.map_err(|e| format!("failed to read {}: {}", resource_path.display(), e))?;
let parsed: Value = serde_json::from_str(&content)
.map_err(|e| format!("invalid JSON: {}", e))?;
parsed["greeting"]
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| "greeting key missing".into())
}
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Step 4 — Call the command from React:
// src/App.tsx
import { useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
function App() {
const [greeting, setGreeting] = useState('');
async function loadGreeting(language: string) {
const message = await invoke<string>('greet', { language });
setGreeting(message);
}
return (
<div>
<button onClick={() => loadGreeting('en')}>English</button>
<button onClick={() => loadGreeting('de')}>Deutsch</button>
<p>{greeting}</p>
</div>
);
}
export default App;
When you click a button, the app resolves the corresponding JSON file from the resource bundle, reads it in Rust, and passes the greeting string back to the frontend. No path arithmetic, no platform conditionals.
Everything is working correctly if…:
You see "Hello!" or "Guten Tag!" appear on screen after clicking a button. If the greeting appears and the developer console shows no errors, your resource access pipeline is set up properly.
Mistakes to Avoid
Not bundling the file first
If you call resolve("lang/en.json", BaseDirectory::Resource) but didn't include that file in tauri.conf.json > bundle > resources, the resolution will fail at runtime. The file must be listed explicitly or matched by a glob pattern.
Using the wrong path syntax in resolve
The string you pass to resolve must follow the same conventions as the resource configuration. A leading ./ is stripped, ../ becomes _up_, and absolute paths get a _root_ prefix. Test your resolve call with a known file before relying on it in production logic.
Calling resolve before the app is initialized
You cannot call handle.path().resolve(...) outside of setup or a command because there is no AppHandle yet. If you need the resource path in code that runs before setup, restructure the initialization to happen inside the setup closure.
Using std::fs on Android
On Android the resolved path is a URI starting with asset://localhost/, which std::fs cannot read. Always use the file system plugin (app.fs().read_to_string(...)) for mobile builds. This error can silently slip through because it only manifests on device or emulator.
What You’ve Learned
You now know that resource files are not magic — they land in a predictable directory, and Tauri gives you consistent APIs to resolve their paths from both Rust and JavaScript. The core pattern is always the same: configure the bundle, resolve with BaseDirectory::Resource, and read the content with the appropriate I/O function.