Common Use Cases - Resources API

Learn how to embed and serve images, fonts, config files, and templates with Tauri’s Resources API using a React frontend

The Resources API solves a concrete problem: you have static files—logos, JSON configs, custom font files, HTML templates—that your app needs at runtime, but you don’t want to ship them loose in the file system or deal with cross-platform path differences. Embedding them into the application binary and accessing them through a secure, platform-agnostic URL is the job of the Resources API.

In practice, this means you can drop a file into your project, list it in the Tauri configuration, and then load it from the frontend with a single async call. No manual file copying, no writable folder permissions. The same mechanism works across Windows, macOS, and Linux without any branching logic.

Configuring Embedded Resources

Every file you want to access through the Resources API must be declared in tauri.conf.json under bundle > resources. You can use glob patterns to include entire folders or specific file types. See Configuration: Resources for the config-side walkthrough.

Open src-tauri/tauri.conf.json and add a resources array. The paths are relative to the src-tauri directory.

src-tauri/tauri.conf.json
{
  "productName": "my-app",
  "version": "1.0.0",
  "build": {
    "frontendDist": "../dist",
    "devUrl": "http://localhost:1420",
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build"
  },
  "app": {
    "security": {
      "assetProtocol": {
        "enable": true,
        "scope": []
      }
    },
    "windows": [
      {
        "title": "Resources Demo",
        "width": 800,
        "height": 600
      }
    ]
  },
  "bundle": {
    "active": true,
    "resources": [
      "assets/images/*",
      "assets/fonts/**",
      "assets/config.json",
      "assets/template.html"
    ]
  }
}

The assetProtocol is enabled by default, so you rarely need to touch it. The scope array can restrict which resources are allowed, but leaving it empty grants access to all declared resources.

Resource file location:

Even though the paths in bundle.resources are relative to src-tauri, it’s cleaner to keep the actual files inside a dedicated src-tauri/assets/ folder to avoid clutter.

Accessing Resources from the Frontend

Once configured, resources become available through the resolveResource function from @tauri-apps/api/resource. Calling it with a relative path (matching what you declared) returns an absolute URL that points to the extracted file. The URL uses a private protocol (asset:// or https://asset.localhost) that the webview can load directly—no CORS, no extra fetch logic.

import { resolveResource } from '@tauri-apps/api/resource';
async function loadLogo() {
  const logoUrl = await resolveResource('assets/images/logo.png');
  return logoUrl; // e.g., "https://asset.localhost/.../logo.png"
}

The returned string is ready to be used in an <img> tag, a CSS url(), or a fetch call. Under the hood, Tauri extracts the embedded file to a temporary directory and hands you the appropriate URL. You never write a file yourself.

Embedding and Displaying Images

Images are the most common use case: splash screens, logos, icons inside the UI. Embedding them means your app won’t break because an image file is missing from a relative path or a CDN.

Place a logo.png inside src-tauri/assets/images/ and reference it in the configuration. Then display it in a React component.

src/App.tsx
import { useState, useEffect } from 'react';
import { resolveResource } from '@tauri-apps/api/resource';
function App() {
  const [logoUrl, setLogoUrl] = useState<string>('');
  useEffect(() => {
    resolveResource('assets/images/logo.png').then(setLogoUrl);
  }, []);
  return (
    <div className="container">
      {logoUrl ? (
        <img src={logoUrl} alt="App Logo" width={200} />
      ) : (
        <p>Loading logo...</p>
      )}
    </div>
  );
}
export default App;

The useEffect runs once on mount, fetches the resource URL, and passes it to the img element. The browser handles decoding; no Rust code required.

No extra permissions needed:

When using the Resources API, you don’t need to declare any file system or path permissions in your capability files. The asset protocol handles the security.

Loading Custom Fonts

Custom fonts often live in @font-face rules and need a URL. Instead of shipping fonts next to the binary or relying on system fonts, you embed them and reference the resolved URL directly in CSS.

Assume you have OpenSans-Regular.woff2 inside src-tauri/assets/fonts/. After declaring the glob pattern assets/fonts/** in the configuration, resolve the font path and inject a style sheet.

src/App.tsx
import { useEffect } from 'react';
import { resolveResource } from '@tauri-apps/api/resource';
function App() {
  useEffect(() => {
    async function loadFont() {
      const fontUrl = await resolveResource('assets/fonts/OpenSans-Regular.woff2');
      const style = document.createElement('style');
      style.textContent = `
        @font-face {
          font-family: 'OpenSans';
          src: url('${fontUrl}') format('woff2');
          font-weight: normal;
          font-style: normal;
        }
        body {
          font-family: 'OpenSans', sans-serif;
        }
      `;
      document.head.appendChild(style);
    }
    loadFont();
  }, []);
  return <h1>This text uses the embedded font.</h1>;
}
export default App;

The font file is extracted only once. After the style is injected, any element on the page can use the custom font-family. The URL is valid for the entire application lifetime.

Font formats and webview compatibility:

Not every webview engine supports the same font formats equally. On Linux (WebKitGTK), WOFF2 is generally fine, but if you need broader support, include WOFF and TrueType fallbacks. Test on the target platforms early.

Reading Configuration Files

Configuration files like JSON, YAML, or TOML often need to be read by the Rust backend to set up app state, feature flags, or connection parameters. The Resources API lets you embed these files, and Rust can access them through the resource directory exposed by the Tauri runtime.

Add assets/config.json to bundle.resources and create the file with some data:

src-tauri/assets/config.json
{
  "apiEndpoint": "https://api.example.com",
  "maxRetries": 3
}

In Rust, use the tauri::Manager trait to obtain the app handle and resolve the resource directory path. Then read the file with standard file I/O.

src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::fs;
use tauri::Manager;
#[tauri::command]
fn get_config(app: tauri::AppHandle) -> Result<serde_json::Value, String> {
    // The resource_dir() method returns the path where all embedded resources are extracted.
    let resource_dir = app
        .path()
        .resource_dir()
        .map_err(|e| format!("Failed to get resource dir: {}", e))?;
    let config_path = resource_dir.join("assets/config.json");
    let contents = fs::read_to_string(&config_path)
        .map_err(|e| format!("Could not read config file: {}", e))?;
    let json: serde_json::Value = serde_json::from_str(&contents)
        .map_err(|e| format!("Invalid JSON: {}", e))?;
    Ok(json)
}
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_config])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Notice that path().resource_dir() is the official way to find the extracted resources in Rust. The directory already exists when the command runs, so a simple read_to_string works.

From the frontend, invoke the command and consume the configuration:

src/App.tsx
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
interface Config {
  apiEndpoint: string;
  maxRetries: number;
}
function App() {
  const [config, setConfig] = useState<Config | null>(null);
  useEffect(() => {
    invoke<Config>('get_config')
      .then(setConfig)
      .catch(console.error);
  }, []);
  return (
    <div>
      {config ? (
        <pre>{JSON.stringify(config, null, 2)}</pre>
      ) : (
        <p>Loading configuration...</p>
      )}
    </div>
  );
}
export default App;

The separation is clean: the frontend never touches the file system, and the backend only reads from a controlled directory.

Resource directory is read-only after extraction:

The extracted resource directory is not meant for writing runtime data. If your app needs to modify a config file, copy it to the app’s data directory on first run. Writing directly to the resource folder will fail or be overwritten on update.

Rendering Templates

Templates—such as HTML reports, email bodies, or document stubs—are another natural fit for the Resources API. The Rust backend can read the template, insert dynamic data, and return the rendered result to the frontend for display or export.

Embed assets/template.html with a placeholder:

src-tauri/assets/template.html
<html>
  <body>
    <h1>Report for {{name}}</h1>
    <p>Generated on {{date}}.</p>
  </body>
</html>

Define a Rust command that loads the template and replaces placeholders. For simplicity, we use straightforward string substitution, but a real project might use a templating crate like tera or handlebars.

src-tauri/src/main.rs
use std::fs;
use tauri::Manager;
#[tauri::command]
fn render_report(app: tauri::AppHandle, name: String) -> Result<String, String> {
    let resource_dir = app
        .path()
        .resource_dir()
        .map_err(|e| format!("Failed to get resource dir: {}", e))?;
    let template_path = resource_dir.join("assets/template.html");
    let mut template = fs::read_to_string(&template_path)
        .map_err(|e| format!("Could not read template: {}", e))?;
    let date = chrono::Local::now().format("%Y-%m-%d").to_string();
    template = template.replace("{{name}}", &name);
    template = template.replace("{{date}}", &date);
    Ok(template)
}

Add chrono to your Cargo.toml dependencies if you use date formatting, or simply hardcode a string for testing.

src-tauri/Cargo.toml
[dependencies]
tauri = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4"

Frontend invocation:

src/App.tsx
import { useState } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
function App() {
  const [html, setHtml] = useState<string>('');
  async function generateReport() {
    try {
      const result = await invoke<string>('render_report', { name: 'Alice' });
      setHtml(result);
    } catch (error) {
      console.error(error);
    }
  }
  return (
    <div>
      <button onClick={generateReport}>Generate Report</button>
      {html && <div dangerouslySetInnerHTML={{ __html: html }} />}
    </div>
  );
}
export default App;

Sanitize user inputs before injecting into templates:

If the template ends up in a webview via dangerouslySetInnerHTML and contains unsanitized user data, you risk XSS vulnerabilities. Always escape or sanitize dynamic content, especially when the template originates from an embedded file that might have been tampered with at build time.

Common Mistakes and How to Avoid Them

Wrong resource path in resolveResource. The path argument must match exactly the relative path from the resource root. If you declared assets/images/logo.png, calling resolveResource('logo.png') will fail. Always use the full path as it appears inside the resource directory.

Missing entry in bundle.resources. A file physically placed in the project does nothing until it’s listed in the configuration. The build will succeed, but resolveResource will throw an error at runtime. Double-check that the path is covered by a glob or an explicit entry.

Assuming the resource directory is writable. The extraction location is meant for reading only. If you need a mutable config, copy it to the app’s data directory (via app.path().app_data_dir()) on first launch.

Not testing on all platforms. The resource extraction path and protocol URLs are consistent, but the underlying file system permissions can vary. A path that works on macOS might fail on a Linux distribution with a strict AppArmor profile. Always test the final bundle, not just the dev server.

Resources in development mode:

During tauri dev, resources are served directly from the source directory without extraction, so changes to the files are reflected immediately. This is convenient for development but can hide path resolution issues that only appear in production builds.


What you’ve seen across images, fonts, configs, and templates is one underlying truth: the Resources API removes the file system from the equation. That changes how you think about asset delivery in a desktop app—no more searching for the right platform-dependent path or worrying about whether a directory exists. Every resource lives inside the binary and gets extracted only when needed, at exactly the place the runtime expects.

This pattern extends naturally to any static file your app relies on: translation files, audio clips, machine learning models (within size constraints), or documentation.