Introduction to the Resources API

Learn what bundled resources are in Tauri v2, when to use them, and how to configure and access them from Rust and a React frontend.

Every desktop application carries more than just code — images, configuration files, HTML templates, and sound files that the program needs to look and behave correctly. In Tauri v2, these files are called resources. They are bundled right into the application binary and can be read at runtime without worrying about file paths on the user's machine. This section explains what resources are, why they exist, how to include them in your app, and how to access them from Rust commands and the React frontend.

What Bundled Resources Are

A resource in Tauri v2 is any static file or asset you declare as part of your app's bundle. When Tauri builds your application, it copies these files into a special resource directory inside the final binary or next to the executable. At runtime, your code can ask the system for the absolute path to that directory and read the files just like any other file on disk — but you never need to guess where they are stored.

This approach solves a real problem in desktop development. If you include images in your source code, they end up in the frontend's dist folder and are served as normal web assets. That works fine for UI icons. But some files should not be publicly exposed through the webview's static file server — or they need to be available to the Rust backend directly. Resources make that separation clean: frontend assets stay in the web context, while resources live in a protected directory that you control explicitly.

Resources vs frontend assets:

The files inside your Vite public directory become regular HTTP assets accessible to the webview. Resources declared in tauri.conf.json end up in a separate, OS‑specific resource directory. Use resources when the Rust backend needs the file, or when you want to keep the file out of the frontend's reach. The Configuration: Resources chapter covers bundling from the config side.

When to Use Resources

You reach for resources when your application needs access to a file that must be present on the user's machine, not fetched from the internet, and whose location should not depend on how the app was installed. Typical situations include:

  • Configuration templates that the Rust backend reads to generate user‑specific config files.
  • HTML or CSS snippets injected into a webview by a Rust command, not served as static assets.
  • Embedded media (images, sounds) used by native features like tray icons or system notifications.
  • Data files such as SQLite database seeds, CSV reference tables, or JSON schemas.
  • Multi‑language assets when the Rust backend needs to pick the right file based on the system locale.

Not every file belongs as a resource. If the file is only ever shown inside the webview UI, placing it inside the Vite frontend project (in public/) is simpler and more natural. Resources are for the border between the Rust core and the webview — where both sides might need the same file, or where only Rust should touch it.

Don't put secrets in resources:

Resources are embedded in the application bundle and can be extracted by anyone with access to the installed app files. Never include API keys, credentials, or private keys as resources. Use environment variables or a secure secrets service instead.

How Resources Are Configured

Resources are declared in the tauri.conf.json file under the bundle.resources key. You provide a list of glob patterns or file paths. Tauri treats each matching file as a resource and places it into the resource directory during the build.

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/**/*"
    ]
  }
}

This configuration tells Tauri to take everything inside the src-tauri/resources/ folder (including subfolders) and bundle it. You are free to choose a different folder name; the key is that the glob path is relative to the src-tauri/ directory.

After a build, the resource directory structure mirrors what you placed there. If you have src-tauri/resources/config/app.json, then at runtime the resource path is config/app.json. The top-level resource folder itself is not part of the path — you only use the relative path inside it.

Incorrect glob patterns break the build:

A glob that accidentally matches thousands of files can blow up your bundle size. Test your patterns with a dry‑run by checking which files get picked up. In development mode, resources are served from the source directory, so missing files will show up as runtime errors immediately.

Accessing Resources from Rust

The Rust backend accesses resources through the tauri::Manager trait. Every Tauri command receives an app_handle (or AppHandle) that gives you a path() resolver. Calling resolve with BaseDirectory::Resource returns an absolute PathBuf you can then use to read the file.

src-tauri/src/lib.rs
#[tauri::command]
fn read_config(app: tauri::AppHandle) -> Result<String, String> {
    let resource_path = app
        .path()
        .resolve("config/app.json", tauri::path::BaseDirectory::Resource)
        .map_err(|e| e.to_string())?;
    std::fs::read_to_string(resource_path).map_err(|e| e.to_string())
}

Here, resolve("config/app.json", BaseDirectory::Resource) looks for a file called config/app.json inside the resource directory. The result is an absolute path that works on every supported platform. The function then reads the file content and returns it to the frontend.

If the resource file does not exist, resolve does not fail — it still returns a path as if the file were there. The real error will come from read_to_string when it can't open the file. Always handle that case gracefully so the user sees a clear message, not a cryptic system error.

In a real application, you would register this command in the Tauri builder:

src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![read_config])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

And you would ensure the command is allowed in a capability file (the default permission set already includes your own commands, but if you have custom permissions, you must include it explicitly).

Accessing Resources from the Frontend

From the React side, you need two pieces: resolveResource from @tauri-apps/api/path to get the resource's absolute file path, and convertFileSrc from @tauri-apps/api/core to turn that path into a URL the webview can load.

src/App.tsx
import { resolveResource } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/core';
import { useEffect, useState } from 'react';
function App() {
  const [logoUrl, setLogoUrl] = useState<string>('');
  useEffect(() => {
    async function loadResource() {
      const resourcePath = await resolveResource('images/logo.png');
      const assetUrl = convertFileSrc(resourcePath);
      setLogoUrl(assetUrl);
    }
    loadResource();
  }, []);
  return (
    <div>
      {logoUrl ? (
        <img src={logoUrl} alt="App logo" />
      ) : (
        <p>Loading logo…</p>
      )}
    </div>
  );
}
export default App;

The flow is straightforward: resolveResource('images/logo.png') gives you a platform‑specific path like /path/to/resource/dir/images/logo.png. convertFileSrc transforms that into asset://localhost/<encoded-path>, which the webview understands. The result is an image tag that displays the embedded resource without any network requests.

A beginner might wonder why two steps are needed. The reason is safety: convertFileSrc works with any file path, but the asset:// protocol only allows access to paths inside the resource directory or specific configured scopes. You cannot accidentally serve arbitrary system files. resolveResource ensures the path is rooted in the resource directory; convertFileSrc then creates the safe URL.

Everything is working if you see the image:

If you add a logo.png to src-tauri/resources/images/ and run the app, the component above should display it. In development mode, Tauri serves resources directly from the file system, so changes to the resource files are picked up instantly without rebuilding.

A Complete End‑to‑End Example

Let's walk through a small, focused example that bundles a text file and a JSON file, then shows them in the React UI using both the Rust backend and direct frontend access.

Step 1: Create the resource files.
Inside your src-tauri/ directory, create a resources/ folder with a data/ subdirectory. Add info.txt with a simple message and config.json with some settings.

src-tauri/resources/data/info.txt
Welcome to Tauri Resources API!
src-tauri/resources/data/config.json
{
  "theme": "dark",
  "language": "en"
}

Step 2: Declare the resources in tauri.conf.json.

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/**/*"
    ]
  }
}

Step 3: Write a Rust command that reads the text file.

src-tauri/src/lib.rs
#[tauri::command]
fn read_info(app: tauri::AppHandle) -> Result<String, String> {
    let resource_path = app
        .path()
        .resolve("data/info.txt", tauri::path::BaseDirectory::Resource)
        .map_err(|e| e.to_string())?;
    std::fs::read_to_string(resource_path).map_err(|e| e.to_string())
}

Step 4: Use the command and the JSON resource in the React frontend.

src/App.tsx
import { resolveResource } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
import { useEffect, useState } from 'react';
interface Config {
  theme: string;
  language: string;
}
function App() {
  const [info, setInfo] = useState('');
  const [config, setConfig] = useState<Config | null>(null);
  useEffect(() => {
    // Fetch the text resource via a Rust command
    invoke<string>('read_info').then(setInfo).catch(console.error);
    // Fetch the JSON resource directly from the frontend
    async function loadConfig() {
      const path = await resolveResource('data/config.json');
      const url = convertFileSrc(path);
      const response = await fetch(url);
      const json: Config = await response.json();
      setConfig(json);
    }
    loadConfig().catch(console.error);
  }, []);
  return (
    <div>
      <h1>Resource Demo</h1>
      <p>Info from Rust: {info}</p>
      {config && (
        <p>
          Config: theme = {config.theme}, language = {config.language}
        </p>
      )}
    </div>
  );
}
export default App;

Step 5: Register the command and run.

src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![read_info])
        .run(tauri::generate_context!())
        .expect("failed to launch app");
}

When the application starts, the text “Welcome to Tauri Resources API!” appears from the Rust command, and the JSON configuration is printed from a frontend fetch. Both source the data from the same resource directory, but each side uses the path in the way that suits it best.

Important Details and Pitfalls

  • Path case sensitivity. Resource paths are case‑sensitive on Linux and macOS. config/App.json is not the same as config/app.json. Stick to a consistent naming convention to avoid surprises when building for different OSes.
  • Large files. Every resource increases the size of your application bundle. Embedding a 100 MB video is rarely a good idea. For large datasets, consider downloading them on first launch or using a sidecar binary that reads external files.
  • Development vs production. In development mode, resources are read directly from the source directory, so they reflect live edits. In production, they are extracted to a temporary location or kept inside the binary depending on the target. Test your app with a production build before releasing.
  • Accessing resources from workers. If you use web workers, they cannot call Tauri APIs directly. You will need to pass the resource URL from the main thread to the worker after resolving it.

Summary

The Resources API gives Tauri apps a simple, cross‑platform mechanism to bundle files that must be available offline, protected from the webview, or accessible from the Rust core. By declaring glob patterns in tauri.conf.json, you control exactly which files become resources, and then you use resolve in Rust or resolveResource/convertFileSrc in JavaScript to turn those declarations into actual file paths and URLs.

The mental model to carry forward is this: assets that only the UI needs stay in the frontend project. Assets that the backend needs, or that both sides share, become resources. This keeps concerns separated and makes the architecture of your application easier to reason about.